Skip to content
This repository has been archived by the owner on Mar 16, 2022. It is now read-only.

Commit

Permalink
Add TCK model test for event-sourced entities
Browse files Browse the repository at this point in the history
  • Loading branch information
pvlugter committed Sep 15, 2020
1 parent d9426d4 commit ce6ea7a
Show file tree
Hide file tree
Showing 13 changed files with 707 additions and 36 deletions.
19 changes: 17 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ lazy val root = (project in file("."))
`protocols`,
`proxy`,
`java-support`,
`java-support-tck`,
`java-shopping-cart`,
`java-pingpong`,
`akka-client`,
Expand Down Expand Up @@ -668,6 +669,20 @@ lazy val `java-support` = (project in file("java-support"))
)
)

lazy val `java-support-tck` = (project in file("java-support/tck"))
.dependsOn(`java-support`, `java-shopping-cart`)
.enablePlugins(AkkaGrpcPlugin, AssemblyPlugin, JavaAppPackaging, DockerPlugin, AutomateHeaderPlugin, NoPublish)
.settings(
name := "cloudstate-java-tck",
dockerSettings,
mainClass in Compile := Some("io.cloudstate.javasupport.tck.JavaSupportTck"),
akkaGrpcGeneratedLanguages := Seq(AkkaGrpc.Java),
PB.protoSources in Compile += (baseDirectory in ThisBuild).value / "protocols" / "tck",
PB.targets in Compile := Seq(PB.gens.java -> (sourceManaged in Compile).value),
javacOptions in Compile ++= Seq("-encoding", "UTF-8", "-source", "1.8", "-target", "1.8"),
assemblySettings("cloudstate-java-tck.jar")
)

lazy val `java-shopping-cart` = (project in file("samples/java-shopping-cart"))
.dependsOn(`java-support`)
.enablePlugins(AkkaGrpcPlugin, AssemblyPlugin, JavaAppPackaging, DockerPlugin, AutomateHeaderPlugin, NoPublish)
Expand Down Expand Up @@ -773,7 +788,7 @@ lazy val `tck` = (project in file("tck"))
),
PB.protoSources in Compile ++= {
val baseDir = (baseDirectory in ThisBuild).value / "protocols"
Seq(baseDir / "protocol")
Seq(baseDir / "protocol", baseDir / "tck")
},
dockerSettings,
Compile / bashScriptDefines / mainClass := Some("org.scalatest.run"),
Expand All @@ -785,7 +800,7 @@ lazy val `tck` = (project in file("tck"))
.flatMap(key => sys.props.get(key).map(value => s"-D$key=$value")),
parallelExecution in IntegrationTest := false,
executeTests in IntegrationTest := (executeTests in IntegrationTest)
.dependsOn(`proxy-core` / assembly, `java-shopping-cart` / assembly)
.dependsOn(`proxy-core` / assembly, `java-support-tck` / assembly)
.value
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ private[impl] trait AbstractEffectContext extends EffectContext {

override final def effect(effect: ServiceCall, synchronous: Boolean): Unit = {
checkActive()
SideEffect(
serviceName = effect.ref().method().getService.getFullName,
commandName = effect.ref().method().getName,
payload = Some(ScalaPbAny.fromJavaProto(effect.message())),
synchronous = synchronous
) :: effects
effects = SideEffect(
serviceName = effect.ref().method().getService.getFullName,
commandName = effect.ref().method().getName,
payload = Some(ScalaPbAny.fromJavaProto(effect.message())),
synchronous = synchronous
) :: effects
}

final def sideEffects: List[SideEffect] = effects.reverse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,17 @@ class EventSourcedImplSpec extends WordSpec with Matchers with BeforeAndAfterAll
entity.send(command(1, "cart", "GetCart"))
entity.expect(reply(1, EmptyCart))
entity.send(command(2, "cart", "AddItem", addItem("abc", "apple", 1)))
entity.expect(reply(2, EmptyJavaMessage, itemAdded("abc", "apple", 1)))
entity.expect(reply(2, EmptyJavaMessage, persist(itemAdded("abc", "apple", 1))))
entity.send(command(3, "cart", "AddItem", addItem("abc", "apple", 2)))
entity.expect(
reply(3, EmptyJavaMessage, Seq(itemAdded("abc", "apple", 2)), cartSnapshot(Item("abc", "apple", 3)))
reply(3,
EmptyJavaMessage,
persist(itemAdded("abc", "apple", 2)).withSnapshot(cartSnapshot(Item("abc", "apple", 3))))
)
entity.send(command(4, "cart", "GetCart"))
entity.expect(reply(4, cart(Item("abc", "apple", 3))))
entity.send(command(5, "cart", "AddItem", addItem("123", "banana", 4)))
entity.expect(reply(5, EmptyJavaMessage, itemAdded("123", "banana", 4)))
entity.expect(reply(5, EmptyJavaMessage, persist(itemAdded("123", "banana", 4))))
entity.passivate()
val reactivated = protocol.eventSourced.connect()
reactivated.send(init(ShoppingCart.Name, "cart", snapshot(3, cartSnapshot(Item("abc", "apple", 3)))))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2019 Lightbend Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.cloudstate.javasupport.tck;

import com.example.shoppingcart.Shoppingcart;
import io.cloudstate.javasupport.CloudState;
import io.cloudstate.javasupport.tck.model.eventsourced.EventSourcedTckModelEntity;
import io.cloudstate.javasupport.tck.model.eventsourced.EventSourcedTwoEntity;
import io.cloudstate.samples.shoppingcart.ShoppingCartEntity;
import io.cloudstate.tck.model.Eventsourced;

public final class JavaSupportTck {
public static final void main(String[] args) throws Exception {
new CloudState()
.registerEventSourcedEntity(
EventSourcedTckModelEntity.class,
Eventsourced.getDescriptor().findServiceByName("EventSourcedTckModel"),
Eventsourced.getDescriptor())
.registerEventSourcedEntity(
EventSourcedTwoEntity.class,
Eventsourced.getDescriptor().findServiceByName("EventSourcedTwo"))
.registerEventSourcedEntity(
ShoppingCartEntity.class,
Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"),
com.example.shoppingcart.persistence.Domain.getDescriptor())
.start()
.toCompletableFuture()
.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2019 Lightbend Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.cloudstate.javasupport.tck.model.eventsourced;

import io.cloudstate.javasupport.Context;
import io.cloudstate.javasupport.ServiceCall;
import io.cloudstate.javasupport.ServiceCallRef;
import io.cloudstate.javasupport.eventsourced.*;
import io.cloudstate.tck.model.Eventsourced.*;
import java.util.Optional;

@EventSourcedEntity(persistenceId = "event-sourced-tck-model", snapshotEvery = 5)
public class EventSourcedTckModelEntity {

private final ServiceCallRef<Request> serviceTwoCall;

private String state = "";

public EventSourcedTckModelEntity(Context context) {
serviceTwoCall =
context
.serviceCallFactory()
.lookup("cloudstate.tck.model.EventSourcedTwo", "Call", Request.class);
}

@Snapshot
public Persisted snapshot() {
return Persisted.newBuilder().setValue(state).build();
}

@SnapshotHandler
public void handleSnapshot(Persisted snapshot) {
state = snapshot.getValue();
}

@EventHandler
public void handleEvent(Persisted event) {
state += event.getValue();
}

@CommandHandler
public Optional<Response> process(Request request, CommandContext context) {
boolean forwarding = false;
for (RequestAction action : request.getActionsList()) {
switch (action.getActionCase()) {
case EMIT:
context.emit(Persisted.newBuilder().setValue(action.getEmit().getValue()).build());
break;
case FORWARD:
forwarding = true;
context.forward(serviceTwoRequest(action.getForward().getId()));
break;
case EFFECT:
Effect effect = action.getEffect();
context.effect(serviceTwoRequest(effect.getId()), effect.getSynchronous());
break;
case FAIL:
context.fail(action.getFail().getMessage());
break;
}
}
return forwarding
? Optional.empty()
: Optional.of(Response.newBuilder().setMessage(state).build());
}

private ServiceCall serviceTwoRequest(String id) {
return serviceTwoCall.createCall(Request.newBuilder().setId(id).build());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2019 Lightbend Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.cloudstate.javasupport.tck.model.eventsourced;

import io.cloudstate.javasupport.eventsourced.*;
import io.cloudstate.tck.model.Eventsourced.*;

@EventSourcedEntity
public class EventSourcedTwoEntity {
public EventSourcedTwoEntity() {}

@CommandHandler
public Response call(Request request) {
return Response.newBuilder().build();
}
}
123 changes: 123 additions & 0 deletions protocols/tck/cloudstate/tck/model/eventsourced.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright 2019 Lightbend Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//
// == Cloudstate TCK model test for event-sourced entities ==
//

syntax = "proto3";

package cloudstate.tck.model;

import "cloudstate/entity_key.proto";

option java_package = "io.cloudstate.tck.model";

//
// The `EventSourcedTckModel` service should be implemented in the following ways:
//
// - The entity persistence-id must be `event-sourced-tck-model`.
// - Snapshots must be configured for every 5 events.
// - The state of the entity is simply a string.
// - Event and snapshot string values are wrapped in `Persisted` messages.
// - The snapshot handler must set the state to the value of a `Persisted` message.
// - The event handler must append the value of a `Persisted` message to the state string.
// - The `Process` method receives a `Request` message with actions to take.
// - Request actions must be processed in order, and can require emitting events, forwarding, side effects, or failing.
// - The `Process` method must reply with the state in a `Response`, after taking actions, unless forwarding or failing.
// - Forwarding and side effects must always be made to the second service `EventSourcedTwo`.
//
service EventSourcedTckModel {
rpc Process(Request) returns (Response);
}

//
// The `EventSourcedTwo` service is only for verifying forward actions and side effects.
// The `Call` method is not required to do anything, and may simply return an empty `Response` message.
//
service EventSourcedTwo {
rpc Call(Request) returns (Response);
}

//
// A `Request` message contains any actions that the entity should process.
// Actions must be processed in order. Any actions after a `Fail` may be ignored.
//
message Request {
string id = 1 [(.cloudstate.entity_key) = true];
repeated RequestAction actions = 2;
}

//
// Each `RequestAction` is one of:
//
// - Emit: emit an event, with a given value.
// - Forward: forward to another service, in place of replying with a Response.
// - Effect: add a side effect to another service to the reply.
// - Fail: fail the current `Process` command.
//
message RequestAction {
oneof action {
Emit emit = 1;
Forward forward = 2;
Effect effect = 3;
Fail fail = 4;
}
}

//
// Emit an event, with the event value in a `Persisted` message.
//
message Emit {
string value = 1;
}

//
// Replace the response with a forward to `cloudstate.tck.model.EventSourcedTwo/Call`.
// The payload must be a `Request` message with the given `id`.
//
message Forward {
string id = 1;
}

//
// Add a side effect to the reply, to `cloudstate.tck.model.EventSourcedTwo/Call`.
// The payload must be a `Request` message with the given `id`.
// The side effect should be marked synchronous based on the given `synchronous` value.
//
message Effect {
string id = 1;
bool synchronous = 2;
}

//
// Fail the current command with the given description `message`.
//
message Fail {
string message = 1;
}

//
// The `Response` message for the `Process` must contain the current state (after processing actions).
//
message Response {
string message = 1;
}

//
// The `Persisted` message wraps both snapshot and event values.
//
message Persisted {
string value = 1;
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class EventSourcedInstrumentationSpec extends AbstractTelemetrySpec {
val event3 = ProtoAny("event", ByteString.copyFromUtf8("event3"))
val snapshot1 = ProtoAny("snapshot", ByteString.copyFromUtf8("snapshot1"))

connection.send(reply(1, reply1, Seq(event1, event2, event3), snapshot1))
connection.send(reply(1, reply1, persist(event1, event2, event3).withSnapshot(snapshot1)))

expectMsg(UserFunctionReply(clientActionReply(messagePayload(reply1))))

Expand Down Expand Up @@ -169,7 +169,7 @@ class EventSourcedInstrumentationSpec extends AbstractTelemetrySpec {
val event4 = ProtoAny("event", ByteString.copyFromUtf8("event4"))
val event5 = ProtoAny("event", ByteString.copyFromUtf8("event5"))

connection.send(reply(2, reply2, event4, event5))
connection.send(reply(2, reply2, persist(event4, event5)))

expectMsg(UserFunctionReply(clientActionReply(messagePayload(reply2))))

Expand Down
Loading

0 comments on commit ce6ea7a

Please sign in to comment.