Skip to content

Commit

Permalink
java-eventing-shopping-cart with codegen (#282)
Browse files Browse the repository at this point in the history
  • Loading branch information
johanandren committed Sep 7, 2021
1 parent d35860f commit 31b7ac1
Show file tree
Hide file tree
Showing 27 changed files with 668 additions and 729 deletions.
16 changes: 8 additions & 8 deletions docs/src/modules/java/pages/actions.adoc
Expand Up @@ -16,14 +16,14 @@ The class implementing an Action needs to be annotated with the `@Action` annota

[source,java,indent=0]
----
include::example$java-eventing-shopping-cart/src/main/java/shopping/product/ToProductPopularityAction.java[tag=annotation]
include::example$java-eventing-shopping-cart/src/main/java/shopping/product/actions/ToProductPopularityServiceAction.java[tag=annotation]
----

Action methods implementing services require the `@Handler` annotation and may have `ActionContext` as their second parameter.

[source,java,indent=2]
----
include::example$java-eventing-shopping-cart/src/main/java/shopping/product/ToProductPopularityAction.java[tag=methods]
include::example$java-eventing-shopping-cart/src/main/java/shopping/product/actions/ToProductPopularityServiceAction.java[tag=methods]
----

To connect the Protobuf service definition with the implementation as an Action, register the implementing class with Akka Serverless:
Expand All @@ -41,19 +41,19 @@ The publisher (or source) may publish an arbitrary number of replies.

=== Forwarding a call from an Action

Action methods that want to forward calls to other service methods use `Reply<T>` as return type. With `Reply<T>` the method can choose to
Action methods that want to forward calls to other service methods use `Effect<T>` as return type. With `Effect<T>` the method can choose to

- reply with a value (`Reply.message`)
- send no reply (`Reply.noReply`)
- forward the call (`Reply.forward`)
- reply with a value (`effects().reply(T)`)
- send no reply (`effects().noReply()`)
- forward the call (`effects().forward()`)

To forward to a different method, look up the target method via the `ActionContext` by specifying the full Protobuf service name, method name and passing the parameter type's class.

[source,java,indent=2]
----
include::java:example$java-eventing-shopping-cart/src/main/java/shopping/product/ToProductPopularityAction.java[tag=forwardRemoved]
include::java:example$java-eventing-shopping-cart/src/main/java/shopping/product/actions/ToProductPopularityServiceAction.java[tag=forwardRemoved]
----
<1> Use `ActionReply<Empty>` as return type and accept an `ActionContext` as second parameter
<1> Use `Effect<Empty>` as return type
<2> Create the parameter the target method accepts
<3> Use the fully-qualified gRPC service name of the target method
<4> Specify the Protobuf rpc method name
Expand Down
30 changes: 8 additions & 22 deletions docs/src/modules/java/pages/eventsourced.adoc
Expand Up @@ -9,7 +9,7 @@ Create an Event Sourced Entity by annotating it with the link:{attachmentsdir}/a

[source,java,indent=0]
----
include::example$java-eventing-shopping-cart/src/main/java/shopping/cart/ShoppingCartEntity.java[tag=class]
include::example$java-eventing-shopping-cart/src/main/java/shopping/cart/domain/ShoppingCartEntity.java[tag=class]
----

The link:{attachmentsdir}/api/com/akkaserverless/javasdk/eventsourcedentity/EventSourcedEntity.html#entityType()[`entityType` {tab-icon}, window="new"] provides a namespace for journal events. Use the simple name for the Entity class. To have a more unique reference, the example above uses `eventsourced-shopping-cart`.
Expand All @@ -34,7 +34,7 @@ Each Entity should store its state locally in a mutable variable, either a mutab

[source,java,indent=4]
----
include::example$java-eventing-shopping-cart/src/main/java/shopping/cart/ShoppingCartEntity.java[tag=state]
include::example$java-eventing-shopping-cart/src/main/java/shopping/cart/domain/ShoppingCartEntity.java[tag=state]
----

== Constructing
Expand All @@ -43,7 +43,7 @@ Akka Serverless constructs instances of the Event Sourced Entity class on demand

[source,java,indent=4]
----
include::example$java-eventing-shopping-cart/src/main/java/shopping/cart/ShoppingCartEntity.java[tag=constructor]
include::example$java-eventing-shopping-cart/src/main/java/shopping/cart/domain/ShoppingCartEntity.java[tag=constructor]
----

== Handling commands
Expand All @@ -56,7 +56,7 @@ The following shows the implementation of the `GetCart` command handler:

[source,java,indent=0]
----
include::example$java-eventing-shopping-cart/src/main/java/shopping/cart/ShoppingCartEntity.java[tag=getCart]
include::example$java-eventing-shopping-cart/src/main/java/shopping/cart/domain/ShoppingCartEntity.java[tag=getCart]
----

== Emitting events
Expand All @@ -67,7 +67,7 @@ IMPORTANT: The **only** way for a command handler to modify Entity state is by e

[source,java,indent=0]
----
include::example$java-eventing-shopping-cart/src/main/java/shopping/cart/ShoppingCartEntity.java[tag=addItem]
include::example$java-eventing-shopping-cart/src/main/java/shopping/cart/domain/ShoppingCartEntity.java[tag=addItem]
----

This command handler also validates the command, ensuring the quantity of items added is greater than zero. Invoking link:{attachmentsdir}/api/com/akkaserverless/javasdk/ClientActionContext.html#fail(java.lang.String)[`context.fail` {tab-icon}, window="new"] fails the command - this method throws - no need to explicitly throw an exception.
Expand All @@ -91,28 +91,14 @@ The following example shows an event handler for the `ItemAdded` event with a ut

[source,java,indent=0]
----
include::example$java-eventing-shopping-cart/src/main/java/shopping/cart/ShoppingCartEntity.java[tag=itemAdded]
include::example$java-eventing-shopping-cart/src/main/java/shopping/cart/domain/ShoppingCartEntity.java[tag=itemAdded]
----

== Producing and handling snapshots
== Snapshots

Snapshots are an important optimization for Event Sourced Entities that emit many events. Rather than reading the entire journal upon loading or restart, Akka Serverless can initiate them from a snapshot.

To produce a snapshot, declare a method annotated with link:{attachmentsdir}/api/com/akkaserverless/javasdk/eventsourcedentity/Snapshot.html[`@Snapshot` {tab-icon}, window="new"]. It takes a context class of type link:{attachmentsdir}/api/com/akkaserverless/javasdk/eventsourcedentity/SnapshotContext.html[`SnapshotContext` {tab-icon}, window="new"], and must return a snapshot of the current state in serializable form.

[source,java,indent=0]
----
include::example$java-eventing-shopping-cart/src/main/java/shopping/cart/ShoppingCartEntity.java[tag=snapshot]
----

When the Event Sourced Entity is loaded again, the snapshot will be loaded before any other events are received, and passed to a snapshot handler. Snapshot handlers are declared by annotating a method with link:{attachmentsdir}/api/com/akkaserverless/javasdk/eventsourcedentity/SnapshotHandler.html[`@SnapshotHandler` {tab-icon}, window="new"], and it can take a context class of type link:{attachmentsdir}/api/com/akkaserverless/javasdk/eventsourcedentity/SnapshotContext.html[`SnapshotContext` {tab-icon}, window="new"].

Multiple snapshot handlers may be defined to handle different types of snapshots. The type matching is done in the same way as for events.

[source,java,indent=0]
----
include::example$java-eventing-shopping-cart/src/main/java/shopping/cart/ShoppingCartEntity.java[tag=handleSnapshot]
----
Snapshots are handled automatically by Akka Serverless without any specific code required. By default, a snapshot is taken for every 100:th event emitted. Tuning how often a snapshot of the state is taken can be configured using config or programmatically handing a `EventSourcedEntityOptions` to the provider when registering the entity.

== Registering the Entity

Expand Down
@@ -1,42 +1,39 @@
/*
* Copyright 2021 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.
/* This code was generated by Akka Serverless tooling.
* As long as this file exists it will not be re-generated.
* You are free to make changes to this file.
*/

package shopping;
package shopping.cart.domain;

import com.akkaserverless.javasdk.testkit.junit.AkkaServerlessTestkitResource;
import shopping.cart.api.ShoppingCartApi;
import shopping.cart.api.ShoppingCartServiceClient;
import com.google.protobuf.Empty;
import org.junit.ClassRule;
import org.junit.Test;

import shopping.Main;
import shopping.cart.api.ShoppingCartApi;
import shopping.cart.api.ShoppingCartServiceClient;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static java.util.concurrent.TimeUnit.*;
import static org.junit.Assert.*;

public class ShoppingCartIntegrationTest {
// Example of an integration test calling our service via the Akka Serverless proxy
// Run all test classes ending with "IntegrationTest" using `mvn verify -Pit`
public class ShoppingCartEntityIntegrationTest {

/**
* The test kit starts both the service container and the Akka Serverless proxy.
*/
@ClassRule
public static final AkkaServerlessTestkitResource testkit =
new AkkaServerlessTestkitResource(Main.createAkkaServerless());
new AkkaServerlessTestkitResource(Main.createAkkaServerless());

/**
* Use the generated gRPC client to call the service through the Akka Serverless proxy.
*/
private final ShoppingCartServiceClient client;

public ShoppingCartIntegrationTest() {
this.client =
ShoppingCartServiceClient.create(testkit.getGrpcClientSettings(), testkit.getActorSystem());
public ShoppingCartEntityIntegrationTest() {
client = ShoppingCartServiceClient.create(testkit.getGrpcClientSettings(), testkit.getActorSystem());
}

ShoppingCartApi.Cart getCart(String cartId) throws Exception {
Expand All @@ -56,7 +53,7 @@ void addItem(String cartId, String productId, String name, int quantity) throws
.setQuantity(quantity)
.build())
.toCompletableFuture()
.get();
.get(2, SECONDS);
}

void removeItem(String cartId, String productId, int quantity) throws Exception {
Expand All @@ -68,7 +65,7 @@ void removeItem(String cartId, String productId, int quantity) throws Exception
.setQuantity(quantity)
.build())
.toCompletableFuture()
.get();
.get(2, SECONDS);
}

ShoppingCartApi.LineItem item(String productId, String name, int quantity) {
Expand Down Expand Up @@ -120,4 +117,4 @@ public void removeItemsFromCart() throws Exception {
cart2.getItemsList(),
List.of(item("b", "Banana", 2)));
}
}
}
@@ -0,0 +1,60 @@
/* This code was generated by Akka Serverless tooling.
* As long as this file exists it will not be re-generated.
* You are free to make changes to this file.
*/
package shopping.product.domain;

import com.akkaserverless.javasdk.testkit.junit.AkkaServerlessTestkitResource;
import com.google.protobuf.Empty;
import org.junit.ClassRule;
import org.junit.Test;
import shopping.Main;
import shopping.product.api.ProductPopularityApi;
import shopping.product.api.ProductPopularityServiceClient;

import static java.util.concurrent.TimeUnit.*;

// Example of an integration test calling our service via the Akka Serverless proxy
// Run all test classes ending with "IntegrationTest" using `mvn verify -Pit`
public class ProductPopularityValueEntityIntegrationTest {

/**
* The test kit starts both the service container and the Akka Serverless proxy.
*/
@ClassRule
public static final AkkaServerlessTestkitResource testkit =
new AkkaServerlessTestkitResource(Main.createAkkaServerless());

/**
* Use the generated gRPC client to call the service through the Akka Serverless proxy.
*/
private final ProductPopularityServiceClient client;

public ProductPopularityValueEntityIntegrationTest() {
client = ProductPopularityServiceClient.create(testkit.getGrpcClientSettings(), testkit.getActorSystem());
}

/*
@Test
public void increaseOnNonExistingEntity() throws Exception {
// TODO: set fields in command, and provide assertions to match replies
// client.increase(ProductPopularityApi.IncreasePopularity.newBuilder().build())
// .toCompletableFuture().get(2, SECONDS);
}
@Test
public void decreaseOnNonExistingEntity() throws Exception {
// TODO: set fields in command, and provide assertions to match replies
// client.decrease(ProductPopularityApi.DecreasePopularity.newBuilder().build())
// .toCompletableFuture().get(2, SECONDS);
}
@Test
public void getPopularityOnNonExistingEntity() throws Exception {
// TODO: set fields in command, and provide assertions to match replies
// client.getPopularity(ProductPopularityApi.GetProductPopularity.newBuilder().build())
// .toCompletableFuture().get(2, SECONDS);
}
*/
}

0 comments on commit 31b7ac1

Please sign in to comment.