Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 101 additions & 2 deletions docs/src/modules/java/pages/replicated-entity-crdt.adoc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
= Implementing Replicated Entities in Java
:page-supergroup-java-scala: Language
:page-aliases: replicated-entity.adoc

include::ROOT:partial$include.adoc[]
Expand All @@ -24,12 +25,14 @@ NOTE: Our Replicated Entity example is a shopping cart service.

The following `shoppingcart_domain.proto` file defines our "Shopping Cart" Replicated Entity. The entity manages line items of a cart and stores these as a <<_replicated_counter_map>>, mapping from each item's product details to its quantity. The counter for each item can be incremented independently in separate Replicated Entity instances and will converge to a total quantity.

[.tabset]
Java::
+
[source,proto]
.src/main/proto/com/example/shoppingcart/domain/shoppingcart_domain.proto
----
include::example$java-replicatedentity-shopping-cart/src/main/proto/com/example/shoppingcart/domain/shoppingcart_domain.proto[]
----

<1> Any classes generated from this protobuf file will be in the Java package `com.example.shoppingcart.domain`.
<2> Import the Akka Serverless protobuf annotations, or options.
<3> Let the messages declared in this protobuf file be inner classes to the Java class `ShoppingCartDomain`.
Expand All @@ -39,10 +42,28 @@ include::example$java-replicatedentity-shopping-cart/src/main/proto/com/example/
<7> `replicated_counter_map` describes the Replicated Data type for this entity.
<8> `key` points to the protobuf message representing the counter map's key type.

Scala::
+
[source,proto]
.src/main/proto/com/example/shoppingcart/domain/shoppingcart_domain.proto
----
include::example$scala-replicatedentity-shopping-cart/src/main/proto/com/example/shoppingcart/domain/shoppingcart_domain.proto[]
----
<1> Any classes generated from this protobuf file will be in the package `com.example.shoppingcart.domain`.
<2> Import the Akka Serverless protobuf annotations, or options.
<3> The protobuf option `(akkaserverless.file).replicated_entity` is specific to code-generation as provided by the Akka Serverless Maven plugin.
<4> `name` denotes the base name for the Replicated Entity. The code-generation will create initial sources `ShoppingCart` and `ShoppingCartIntegrationTest`. Once these files exist, they are not overwritten, so you can freely add logic to them.
<5> `entity_type` is a unique identifier for data replication. The entity name may be changed even after data has been created, the `entity_type` can't be changed.
<6> `replicated_counter_map` describes the Replicated Data type for this entity.
<7> `key` points to the protobuf message representing the counter map's key type.

NOTE: Each Replicated Entity is associated with one underlying Replicated Data type. Replicated Data types that are generic, accepting type parameters for key, value, or element types, are used with `protobuf` messages and can represent structured data. In this shopping cart example, the keys of the counter map are products that have an id and name.

The `shoppingcart_api.proto` file defines the commands we can send to the shopping cart service to manipulate or access the cart's state. They make up the service API:

[.tabset]
Java::
+
[source,proto]
.src/main/proto/com/example/shoppingcart/shoppingcart_api.proto
----
Expand All @@ -57,18 +78,45 @@ include::example$java-replicatedentity-shopping-cart/src/main/proto/com/example/
<7> The service descriptor shows the API of the entity. It lists the methods a client can use to issue Commands to the entity.
<8> The protobuf option `(akkaserverless.service)` is specific to code-generation as provided by the Akka Serverless Maven plugin and points to the protobuf definition `ShoppingCart` we've seen above (in the `com.example.shoppingcart.domain` package).

Scala::
+
[source,proto]
.src/main/proto/com/example/shoppingcart/shoppingcart_api.proto
----
include::example$scala-replicatedentity-shopping-cart/src/main/proto/com/example/shoppingcart/shoppingcart_api.proto[]
----
<1> Any classes generated from this protobuf file will be in the Java package `com.example.shoppingcart`.
<2> Import the Akka Serverless protobuf annotations, or options.
<3> We use protobuf messages to describe the Commands that our service handles. They may contain other messages to represent structured data.
<4> Every Command must contain a `string` field that contains the entity ID and is marked with the `(akkaserverless.field).entity_key` option.
<5> Messages describe the return value for our API. For methods that don't have return values, we use `google.protobuf.Empty`.
<6> The service descriptor shows the API of the entity. It lists the methods a client can use to issue Commands to the entity.
<7> The protobuf option `(akkaserverless.service)` is specific to code-generation as provided by the Akka Serverless Maven plugin and points to the protobuf definition `ShoppingCart` we've seen above (in the `com.example.shoppingcart.domain` package).


== Implementing behavior

A Replicated Entity implementation is a Java class where you define how each command is handled. The class `ShoppingCart` gets generated for us based on the `shoppingcart_api.proto` and `shoppingcart_domain.proto` definitions. Once the `ShoppingCart.java` file exist, it is not overwritten, so you can freely add logic to it. `ShoppingCart` extends the generated class `AbstractShoppingCart` which we're not supposed to change as it gets regenerated in case we update the protobuf descriptors. `AbstractShoppingCart` contains all method signatures corresponding to the API of the service. If you change the API you will see compilation errors in the `ShoppingCart` class and you have to implement the methods required by `AbstractShoppingCart`.
A Replicated Entity implementation is a Java class where you define how each command is handled. The class `ShoppingCart` gets generated for us based on the `shoppingcart_api.proto` and `shoppingcart_domain.proto` definitions. Once the [.group-java]#`ShoppingCart.java`# [.group-scala]# file `ShoppingCart.scala`# exist, it is not overwritten, so you can freely add logic to it. `ShoppingCart` extends the generated class `AbstractShoppingCart` which we're not supposed to change as it gets regenerated in case we update the protobuf descriptors. `AbstractShoppingCart` contains all method signatures corresponding to the API of the service. If you change the API you will see compilation errors in the `ShoppingCart` class and you have to implement the methods required by `AbstractShoppingCart`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, neat, I didn't know it is possible to switch in-text-things with [.group-java]# like that


[.tabset]
Java::
+
[source,java]
.src/main/java/com/example/shoppingcart/domain/ShoppingCart.java
----
include::example$java-replicatedentity-shopping-cart/src/main/java/com/example/shoppingcart/domain/ShoppingCart.java[tag=class]
----
<1> Extends the generated `AbstractShoppingCart`, which extends link:{attachmentsdir}/api/com/akkaserverless/javasdk/replicatedentity/ReplicatedCounterMapEntity.html[`ReplicatedCounterMapEntity` {tab-icon}, window="new"].

Scala::
+
[source,scala]
.src/main/scala/com/example/shoppingcart/domain/ShoppingCart.scala
----
include::example$scala-replicatedentity-shopping-cart/src/main/scala/com/example/shoppingcart/domain/ShoppingCart.scala[tag=class]
----
<1> Extends the generated `AbstractShoppingCart`, which extends link:{attachmentsdir}/scala-api/com/akkaserverless/scalasdk/replicatedentity/ReplicatedCounterMapEntity.html[`ReplicatedCounterMapEntity` {tab-icon}, window="new"].

We need to implement all methods our Replicated Entity offers as https://developer.lightbend.com/docs/akka-serverless/reference/glossary.html#command_handler[_command handlers_].

The code-generation will generate an implementation class with an initial empty implementation which we'll discuss below.
Expand All @@ -83,6 +131,9 @@ In the example below, the `AddItem` service call uses the request message `AddLi

IMPORTANT: The **only** way for a command handler to modify the underlying data for a Replicated Entity is by returning an update effect with an updated Replicated Data object. Note that Replicated Data objects are immutable, with each modifying method returning a new instance of the Replicated Data type.

[.tabset]
Java::
+
[source,java,indent=0]
.src/main/java/com/example/shoppingcart/domain/ShoppingCart.java
----
Expand All @@ -94,12 +145,28 @@ include::example$java-replicatedentity-shopping-cart/src/main/java/com/example/s
<4> We update the underlying data for the Replicated Entity by returning an `Effect` with `effects().update` and the updated data object.
<5> An acknowledgment that the command was successfully processed is sent with a reply message.

Scala::
+
[source,scala,indent=0]
.src/main/scala/com/example/shoppingcart/domain/ShoppingCart.scala
----
include::example$scala-replicatedentity-shopping-cart/src/main/scala/com/example/shoppingcart/domain/ShoppingCart.scala[tag=addItem]
----
<1> The validation ensures quantity of items added is greater than zero and it fails calls with illegal values by returning an `Effect` with `effects.error`.
<2> From the current incoming `AddLineItem` we create a new `Product` object to represent the item's key in the counter map.
<3> We increment the counter for this item in the cart. A new counter will be created if the cart doesn't contain this item already.
<4> We update the underlying data for the Replicated Entity by returning an `Effect` with `effects.update` and the updated data object.
<5> An acknowledgment that the command was successfully processed is sent with a reply message.

=== Retrieving state

The following example shows the implementation of the `GetCart` command handler. This command handler is a read-only command handler--it doesn't update the state, it just returns it.

IMPORTANT: The state of Replicated Entities is eventually consistent. An individual Replicated Entity instance may have an out-of-date value, if there are concurrent modifications made by another instance.

[.tabset]
Java::
+
[source,java,indent=0]
.src/main/java/com/example/shoppingcart/domain/ShoppingCart.java
----
Expand All @@ -108,32 +175,64 @@ include::example$java-replicatedentity-shopping-cart/src/main/java/com/example/s
<1> The current data is passed to the method. Note that this may not be the most up-to-date value, with concurrent modifications made by other instances of this Replicated Entity being replicated eventually.
<2> We convert the domain representation to the API representation that is sent as a reply by returning an `Effect` with `effects().reply`.

Scala::
+
[source,scala,indent=0]
.src/main/scala/com/example/shoppingcart/domain/ShoppingCart.scala
----
include::example$scala-replicatedentity-shopping-cart/src/main/scala/com/example/shoppingcart/domain/ShoppingCart.scala[tag=getCart]
----
<1> The current data is passed to the method. Note that this may not be the most up-to-date value, with concurrent modifications made by other instances of this Replicated Entity being replicated eventually.
<2> We convert the domain representation to the API representation that is sent as a reply by returning an `Effect` with `effects.reply`.

=== Deleting state

The following example shows the implementation of the `RemoveCart` command handler. Replicated Entity instances for a particular entity identifier can be deleted, using a delete `Effect`. Once deleted, an entity instance cannot be recreated, and all subsequent commands for that entity identifier will be rejected with an error.

IMPORTANT: Caution should be taken with creating and deleting Replicated Entities, as Akka Serverless maintains the replicated state in memory and also retains tombstones for each deleted entity. Over time, if many Replicated Entities are created and deleted, this will result in hitting memory limits.

[.tabset]
Java::
+
[source,java,indent=0]
.src/main/java/com/example/shoppingcart/domain/ShoppingCart.java
----
include::example$java-replicatedentity-shopping-cart/src/main/java/com/example/shoppingcart/domain/ShoppingCart.java[tag=removeCart]
----
<1> The Replicated Entity instances for the associated entity key are deleted by using `effects().delete`.

Scala::
+
[source,scala,indent=0]
.src/main/scala/com/example/shoppingcart/domain/ShoppingCart.scala
----
include::example$scala-replicatedentity-shopping-cart/src/main/scala/com/example/shoppingcart/domain/ShoppingCart.scala[tag=removeCart]
----
<1> The Replicated Entity instances for the associated entity key are deleted by using `effects.delete`.

== Registering the Entity

To make Akka Serverless aware of the Replicated Entity, we need to register it with the service.

From the code-generation, the registration gets automatically inserted in the generated `AkkaServerlessFactory.withComponents` method from the `Main` class.

[.tabset]
Java::
+
[source,java]
.src/main/java/com/example/shoppingcart/Main.java
----
include::example$java-replicatedentity-shopping-cart/src/main/java/com/example/shoppingcart/Main.java[]
----

Scala::
+
[source,scala]
.src/main/scala/com/example/shoppingcart/Main.scala
----
include::example$scala-replicatedentity-shopping-cart/src/main/scala/com/example/shoppingcart/Main.scala[]
----

By default, the generated constructor has a `ReplicatedEntityContext` parameter, but you can change this to accept other parameters. If you change the constructor of the `ShoppingCart` class you will see a compilation error here, and you have to adjust the factory function that is passed to `AkkaServerlessFactory.withComponents`.

When more components are added, the `AkkaServerlessFactory` is regenerated and you have to adjust the registration from the `Main` class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import "akkaserverless/annotations.proto"; // <2>
// Describes how this domain relates to a replicated entity
option (akkaserverless.file).replicated_entity = { // <3>
name: "ShoppingCart" // <4>
entity_type: "shopping-cart" // <4>
entity_type: "shopping-cart" // <5>
replicated_counter_map: { // <6>
key: "Product" // <7>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import com.google.protobuf.empty.Empty

/** A replicated entity. */
// tag::class[]
class ShoppingCart(context: ReplicatedEntityContext) extends AbstractShoppingCart {
class ShoppingCart(context: ReplicatedEntityContext) extends AbstractShoppingCart { // <1>
// end::class[]

/** Command handler for "AddItem". */
Expand Down