From cfd7cc8ff947c63319b6671c6caa677cbad015f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Tue, 12 Dec 2023 15:34:06 +0100 Subject: [PATCH 1/7] Started on guide page --- akka-edge-docs/src/main/paradox/guide.md | 3 +- .../main/paradox/guide/5-charging-station.md | 32 +++++++++++++++++++ ...ervices.md => 6-deploying-the-services.md} | 0 .../main/java/charging/ChargingStation.java | 7 ++-- .../main/scala/charging/ChargingStation.scala | 6 ++-- 5 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 akka-edge-docs/src/main/paradox/guide/5-charging-station.md rename akka-edge-docs/src/main/paradox/guide/{5-deploying-the-services.md => 6-deploying-the-services.md} (100%) diff --git a/akka-edge-docs/src/main/paradox/guide.md b/akka-edge-docs/src/main/paradox/guide.md index 25f54c502..1b2243cc9 100644 --- a/akka-edge-docs/src/main/paradox/guide.md +++ b/akka-edge-docs/src/main/paradox/guide.md @@ -18,7 +18,8 @@ the local center to pick up available orders closest to their location. 2. [Coarse Grained Location Replication](guide/2-drone-location-to-cloud-service.md) 3. [Restaurant Deliveries Service](guide/3-restaurant-deliveries-service.md) 4. [Local Drone Delivery Selection](guide/4-local-drone-delivery-selection.md) -5. [Deploying the Services](guide/5-deploying-the-services.md) +5. [Drone Charging Station](guide/5-charging-station.md) +6. [Deploying the Services](guide/6-deploying-the-services.md) @@@ diff --git a/akka-edge-docs/src/main/paradox/guide/5-charging-station.md b/akka-edge-docs/src/main/paradox/guide/5-charging-station.md new file mode 100644 index 000000000..af229dc9b --- /dev/null +++ b/akka-edge-docs/src/main/paradox/guide/5-charging-station.md @@ -0,0 +1,32 @@ +# Drone Charging Station + +To showcase active-active replication between edge and cloud we'll now add a @extref[Replicated Event Sourced Entity](akka-projection:grpc-replicated-event-sourcing-transport.html) +in the form of a charging station. The charging stations are created with a location id placing them in one of the +local-drone-control edge services where the entity is replicated. Drones in that location can request to charge in the +charging station, and be charged if there is a free charging slot. + +## Charging station entity + +The API for creating a replicated event sourced behavior extends the regular event sourced behavior. It accepts three +different commands from the outside `Create` to initialize a charging station, `StartCharging` to start a charging session +for a drone if possible and `GetState` to query the station for its current state. There is also a private `ChargingCompleted` +command that the entity can send to itself. + +The `Create` command leads to a `Created` event which is persisted and initialized the charging station. + +When a slot is free and a drone requests charging a `ChargingStarted` event is persisted and once charging a drone has +completed a `ChargingCompleted` event is persisted: + +Scala +: @@snip [ChargingStation.scala](/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala) { #commands #events } + +Java +: @@snip [ChargingStation.java](/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java) { #commands #events } + + + +The charging station is a simplified replicated entity in that it doesn't really expect any possible conflicts, stations +are created in the central cloud and replicated to the edge, updates related to drones currently charging in the station +happen at the edge and are replicated to the cloud. Akka replicated event sourcing provides APIs for both CRDTs where +conflicts are automatically handled by the data structure and business domain level conflict resolution. For more details +about see the @extref[Akka documentation](akka:replicated-eventsourcing.html). \ No newline at end of file diff --git a/akka-edge-docs/src/main/paradox/guide/5-deploying-the-services.md b/akka-edge-docs/src/main/paradox/guide/6-deploying-the-services.md similarity index 100% rename from akka-edge-docs/src/main/paradox/guide/5-deploying-the-services.md rename to akka-edge-docs/src/main/paradox/guide/6-deploying-the-services.md diff --git a/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java b/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java index 3839a7eac..66182bd69 100644 --- a/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java +++ b/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java @@ -26,7 +26,7 @@ public class ChargingStation extends ReplicatedEventSourcedBehavior< ChargingStation.Command, ChargingStation.Event, ChargingStation.State> { - // commands and replies + // #commands public interface Command extends CborSerializable {} public static final class Create implements Command { @@ -81,8 +81,9 @@ public CompleteCharging(String droneId, ActorRef> replyTo) { this.replyTo = replyTo; } } + // #commands - // events + // #events public interface Event extends CborSerializable {} public static final class Created implements Event { @@ -125,7 +126,7 @@ public ChargingDrone(String droneId, Instant expectedComplete, String replicaId) this.replicaId = replicaId; } } - + // #events public static final class State implements CborSerializable { public final int chargingSlots; public final Set dronesCharging; diff --git a/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala b/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala index 36ac28fb0..c8b35ab1a 100644 --- a/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala +++ b/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala @@ -27,7 +27,7 @@ import scala.concurrent.duration._ object ChargingStation { - // commands and replies + // #commands sealed trait Command extends CborSerializable case class Create( locationId: String, @@ -49,8 +49,9 @@ object ChargingStation { droneId: String, reply: ActorRef[StatusReply[Done]]) extends Command + // #commands - // events + // #events sealed trait Event extends CborSerializable case class Created(locationId: String, chargingSlots: Int) extends Event case class ChargingStarted(droneId: String, expectedComplete: Instant) @@ -58,6 +59,7 @@ object ChargingStation { with StartChargingResponse case class ChargingCompleted(droneId: String) extends Event + // #events case class ChargingDrone( droneId: String, From 0cffd4aacd1a8578f79b51e8682c2bd9b438b796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Tue, 12 Dec 2023 17:55:36 +0100 Subject: [PATCH 2/7] Guide first iteration done --- .../main/paradox/guide/5-charging-station.md | 252 +++++++++++++++++- .../main/java/charging/ChargingStation.java | 12 +- .../java/local/drones/DroneServiceImpl.java | 3 +- .../main/scala/charging/ChargingStation.scala | 12 + .../scala/local/drones/DroneServiceImpl.scala | 3 + .../src/main/java/central/Main.java | 3 + .../src/main/scala/central/Main.scala | 2 + 7 files changed, 275 insertions(+), 12 deletions(-) diff --git a/akka-edge-docs/src/main/paradox/guide/5-charging-station.md b/akka-edge-docs/src/main/paradox/guide/5-charging-station.md index af229dc9b..3e160329e 100644 --- a/akka-edge-docs/src/main/paradox/guide/5-charging-station.md +++ b/akka-edge-docs/src/main/paradox/guide/5-charging-station.md @@ -5,12 +5,13 @@ in the form of a charging station. The charging stations are created with a loca local-drone-control edge services where the entity is replicated. Drones in that location can request to charge in the charging station, and be charged if there is a free charging slot. -## Charging station entity +## Implementing the charging station entity -The API for creating a replicated event sourced behavior extends the regular event sourced behavior. It accepts three -different commands from the outside `Create` to initialize a charging station, `StartCharging` to start a charging session -for a drone if possible and `GetState` to query the station for its current state. There is also a private `ChargingCompleted` -command that the entity can send to itself. +### Commands and events + +The charging station accepts three different commands from the outside `Create` to initialize a charging station, +`StartCharging` to start a charging session for a drone if possible and `GetState` to query the station for its current +state. There is also a private `CompleteCharging` command that only the entity can create. The `Create` command leads to a `Created` event which is persisted and initialized the charging station. @@ -23,10 +24,241 @@ Scala Java : @@snip [ChargingStation.java](/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java) { #commands #events } +### State + +The state of the charging station starts as @java[`null`]@scala[`None`] and requires a `Create` message for the station +to be initialized with a @java[`State`]@scala[`Some(State)`]. + +The `State` contains the number of charging slots that can concurrently charge drones and a set of currently charging drones. + +The state also contains a location id identifying where it is, matching the location id structure of +the local-drone-control service. This is needed so that the station can be replicated only to the edge location where +it is located. + +Scala +: @@snip [ChargingStation.scala](/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala) { #state } + +Java +: @@snip [ChargingStation.java](/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java) { #state } + +### Command handler + +The command handler is in fact two separate handlers, one for when the entity is not yet initialized, only accepting +the `Create` command, and one that is used to handle commands once the station has been initialized. + +The `StartCharging` command is the only one that requires more complicated logic: if a slot is free, persist a `ChargingStarted` +event and tell the drone when charging will be done. A timer is also set, to send a `CompleteCharging` to itself once the +charging time has passed. If all charging slots are busy the reply will instead be when the first slot will +be free again and the drone can come back and try charge again. + +Scala +: @@snip [ChargingStation.scala](/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala) { #commandHandler } + +Java +: @@snip [ChargingStation.java](/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java) { #commandHandler } + +### Event handler + +Just like the command handler the event handler is different depending on if there is state or not. If there is no state +only the `Created` event is accepted. + +Once initialized the charging station expects `ChargingStarted` and `ChargingCompleted` events, additional `Created` +events are ignored. + +Scala +: @@snip [ChargingStation.scala](/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala) { #eventHandler } + +Java +: @@snip [ChargingStation.java](/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java) { #eventHandler } + +The charging station is a very limited replicated entity example to keep the guide simple. It doesn't expect any possible +conflicts, stations are created, once, in the central cloud and replicated to the edge, updates related to drones currently +charging in the station happen at the edge and are replicated to the cloud. Akka replicated event sourcing provides APIs +for both CRDTs where conflicts are automatically handled by the data structure and business domain level conflict resolution. +For more details about see the @extref[Akka documentation](akka:replicated-eventsourcing.html). + + +### Tagging based on location + +To be able to control where the charging station is replicated we tag the events using the location id from the state as +a topic: + +Scala +: @@snip [ChargingStation.scala](/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala) { #tagging } + +Java +: @@snip [ChargingStation.java](/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java) { #tagging } + +### Additional logic on start + +In case the entity is stopped or entire local-drone-control system is shut down, we also need to schedule completion of +current charging sessions when it completes starting up, this is done with a signal handler for the `RecoveryCompleted` signal +which Akka will send to it every time it has completed starting up. Note how we are using the `ReplicationContext` to only +do this on the replica where the charging was started: + +Scala +: @@snip [ChargingStation.scala](/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala) { #eventHandler } + +Java +: @@snip [ChargingStation.java](/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java) { #eventHandler } + +### Serialization + +The state and events of the entity must be serializable because they are written to the datastore, if the local drone control needs to scale out across several nodes to handle traffic, the commands would also be sent between nodes within the Akka cluster. The sample project includes built-in CBOR serialization using the @extref[Akka Serialization Jackson module](akka:serialization-jackson.html). This section describes how serialization is implemented. You do not need to do anything specific to take advantage of CBOR, but this section explains how it is included. + +The state, commands and events are marked as `akka.serialization.jackson.CborSerializable` which is configured to use the built-in CBOR serialization. + +### Setting up replication for the charging station + +Setup for the cloud replica and the edge node differs slightly. + +For the restaurant-drone-deliveries service running in the cloud we set up a `ReplicationSettings` with edge replication +enabled: + +Scala +: @@snip [ChargingStation.scala](/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala) { #replicaInit } + +Java +: @@snip [ChargingStation.java](/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java) { #replicaInit } + +Since we have other events going over akka-projection-grpc producer push replication already in the restaurant-drone-deliveries service +we need to combine all such sources and destinations into single gRPC services: + +Scala +: @@snip [Main.scala](/samples/grpc/restaurant-drone-deliveries-service-scala/src/main/scala/central/Main.scala) { #replicationEndpoint } + +Java +: @@snip [Main.java](/samples/grpc/restaurant-drone-deliveries-service-java/src/main/java/central/Main.java) { #replicationEndpoint } + +For the local-drone-control service we also create `ReplicationSettings` but pass them to a separate initialization method `Replication.grpcEdgeReplication`. +Since the edge node will be responsible for creating connections, no gRPC services needs to be bound: + +Scala +: @@snip [ChargingStation.scala](/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala) { #edgeReplicaInit } + +Java +: @@snip [ChargingStation.java](/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java) { #edgeReplicaInit } + +The returned `EdgeReplication` instance gives us access to a `entityRefFactory` for sending messages to the charging stations. + + +## Service for interacting with the charging station + +In the restaurant-drone-deliveries service we introduce a separate gRPC endpoint for creating and looking at charging +station state: + +Scala +: @@snip [charging_station_api.proto](/samples/grpc/restaurant-drone-deliveries-service-java/src/main/protobuf/charging/charging_station_api.proto) { } + +Java +: @@snip [charging_station_api.proto](/samples/grpc/restaurant-drone-deliveries-service-scala/src/main/protobuf/charging/charging_station_api.proto) { } + +The service implementation takes the `entityRefFactory` as a constructor parameter and uses that to create `EntityRef` instances +for specific charging stations to interact with them: + +Scala +: @@snip [ChargingStationServiceImpl.scala](/samples/grpc/restaurant-drone-deliveries-service-scala/src/main/scala/charging/ChargingStationServiceImpl.scala) { } + +Java +: @@snip [ChargingStationServiceImpl.java](/samples/grpc/restaurant-drone-deliveries-service-java/src/main/java/charging/ChargingStationServiceImpl.java) { } + +## Interacting with the charging station at the edge + +The local-drone-control service does not contain the full gRPC API for interaction but instead has a single `goCharge` method +in the drone gRPC service: + +Scala +: @@snip [DroneServiceImpl.scala](/samples/grpc/local-drone-control-scala/src/main/scala/local/drones/DroneServiceImpl.scala) { #charging } + +Java +: @@snip [DroneServiceImpl.java](/samples/grpc/local-drone-control-java/src/main/java/local/drones/DroneServiceImpl.java) { #charging } + + +## Running + +The complete sample can be downloaded from GitHub, but note that it also includes the next steps of the guide: + +* Java: https://github.com/akka/akka-projection/tree/main/samples/grpc/restaurant-drone-deliveries-service-java +* Scala: https://github.com/akka/akka-projection/tree/main/samples/grpc/restaurant-drone-deliveries-service-scala + +As this service consumes events from the service built in the previous step, start the local-drone-control service first: + +@@@ div { .group-scala } + +To start the local-drone-control-service: + +```shell +sbt run +``` + +@@@ + +@@@ div { .group-java } + +```shell +mvn compile exec:exec +``` + +@@@ + +Then start the drone-restaurant-deliveries-service. + +As the service needs a PostgreSQL instance running, start that up in a docker container and create the database +schema if you did not do that in a previous step of the guide: + +```shell +docker compose up --wait +docker exec -i postgres_db psql -U postgres -t < ddl-scripts/create_tables.sql +``` + +Then start the service: + +@@@ div { .group-scala } + +```shell +sbt -Dconfig.resource=local1.conf run +``` + +And optionally one or two more Akka cluster nodes, but note that the local drone controls +are statically configured to the gRPC port of the first and will only publish events to that node. + +```shell +sbt -Dconfig.resource=local2.conf run +sbt -Dconfig.resource=local3.conf run +``` + +@@@ + +@@@ div { .group-java } + +```shell +mvn compile exec:exec -DAPP_CONFIG=local1.conf +``` + +And optionally one or two more Akka cluster nodes, but note that the local drone controls +are statically configured to the gRPC port of the first and will only publish events to that node. + +```shell +mvn compile exec:exec -DAPP_CONFIG=local2.conf +mvn compile exec:exec -DAPP_CONFIG=local3.conf +``` + +@@@ + +Set up a replicated charging station in the cloud service using [grpcurl](https://github.com/fullstorydev/grpcurl): + +```shell +grpcurl -d '{"charging_station_id":"station1","location_id": "sweden/stockholm/kungsholmen", "charging_slots": 4}' -plaintext localhost:8101 charging.ChargingStationService.CreateChargingStation +``` + +Ask to charge the drone with id `drone1` at the charging station in the edge service: + +```shell +grpcurl -d '{"drone_id":"drone1","charging_station_id":"station1"}' -plaintext 127.0.0.1:8080 local.drones.DroneService.GoCharge +``` +Inspect the state of the charging station in the cloud service to see the charging drone replicated there: -The charging station is a simplified replicated entity in that it doesn't really expect any possible conflicts, stations -are created in the central cloud and replicated to the edge, updates related to drones currently charging in the station -happen at the edge and are replicated to the cloud. Akka replicated event sourcing provides APIs for both CRDTs where -conflicts are automatically handled by the data structure and business domain level conflict resolution. For more details -about see the @extref[Akka documentation](akka:replicated-eventsourcing.html). \ No newline at end of file +```shell +grpcurl -d '{"charging_station_id":"station1"}' -plaintext localhost:8101 charging.ChargingStationService.GetChargingStationState +``` \ No newline at end of file diff --git a/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java b/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java index 66182bd69..fa34eccd8 100644 --- a/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java +++ b/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java @@ -127,6 +127,8 @@ public ChargingDrone(String droneId, Instant expectedComplete, String replicaId) } } // #events + + // #state public static final class State implements CborSerializable { public final int chargingSlots; public final Set dronesCharging; @@ -138,6 +140,7 @@ public State(int chargingSlots, Set dronesCharging, String statio this.stationLocationId = stationLocationId; } } + // #state public static final String ENTITY_TYPE = "charging-station"; @@ -147,6 +150,7 @@ public State(int chargingSlots, Set dronesCharging, String statio * Init for running in edge node, this is the only difference from the ChargingStation in * restaurant-deliveries-service */ + // #edgeReplicaInit public static EdgeReplication initEdge(ActorSystem system, String locationId) { var replicationSettings = ReplicationSettings.create( @@ -159,8 +163,10 @@ public static EdgeReplication initEdge(ActorSystem system, String lo new ConsumerFilter.IncludeTopics(Set.of(locationId)))); return Replication.grpcEdgeReplication(replicationSettings, ChargingStation::create, system); } + // #edgeReplicaInit /** Init for running in cloud replica */ + // #replicaInit public static Replication init(ActorSystem system) { var replicationSettings = ReplicationSettings.create( @@ -169,6 +175,7 @@ public static Replication init(ActorSystem system) { .withEdgeReplication(true); return Replication.grpcReplication(replicationSettings, ChargingStation::create, system); } + // #replicaInit public static Behavior create( ReplicatedBehaviors replicatedBehaviors) { @@ -195,9 +202,9 @@ public State emptyState() { return null; } + // #commandHandler @Override public CommandHandler commandHandler() { - var noStateHandler = newCommandHandlerBuilder() .forNullState() @@ -310,6 +317,7 @@ private Effect handleCompleteCharging( StatusReply.error( "Drone " + completeCharging.droneId + " is not currently charging.")); } + // #commandHandler @Override public EventHandler eventHandler() { @@ -360,9 +368,11 @@ public EventHandler eventHandler() { return noStateHandler.orElse(initializedStateHandler).build(); } + // #tagging @Override public Set tagsFor(State state, Event event) { if (state == null) return Set.of(); else return Set.of("t:" + state.stationLocationId); } + // #tagging } diff --git a/samples/grpc/local-drone-control-java/src/main/java/local/drones/DroneServiceImpl.java b/samples/grpc/local-drone-control-java/src/main/java/local/drones/DroneServiceImpl.java index ec8f8391d..b5e689b4f 100644 --- a/samples/grpc/local-drone-control-java/src/main/java/local/drones/DroneServiceImpl.java +++ b/samples/grpc/local-drone-control-java/src/main/java/local/drones/DroneServiceImpl.java @@ -122,6 +122,7 @@ public CompletionStage completeDelivery(CompleteDelive return convertError(reply); } + // #charging @Override public CompletionStage goCharge(GoChargeRequest in) { logger.info("Requesting charge of {} from {}", in.getDroneId(), in.getChargingStationId()); @@ -176,7 +177,7 @@ public CompletionStage completeCharge(CompleteChargeRe return convertError(response); } - + // #charging private static Timestamp instantToProtoTimestamp(Instant instant) { return Timestamp.newBuilder() .setSeconds(instant.getEpochSecond()) diff --git a/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala b/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala index c8b35ab1a..cb325f004 100644 --- a/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala +++ b/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala @@ -61,6 +61,7 @@ object ChargingStation { case class ChargingCompleted(droneId: String) extends Event // #events + // #state case class ChargingDrone( droneId: String, expectedComplete: Instant, @@ -70,6 +71,7 @@ object ChargingStation { dronesCharging: Set[ChargingDrone], stationLocationId: String) extends CborSerializable + // #state val EntityType = "charging-station" @@ -79,6 +81,7 @@ object ChargingStation { * Init for running in edge node, this is the only difference from the ChargingStation * in restaurant-deliveries-service */ + // #edgeReplicaInit def initEdge(locationId: String)( implicit system: ActorSystem[_]): EdgeReplication[Command] = { val replicationSettings = @@ -91,10 +94,12 @@ object ChargingStation { ConsumerFilter.IncludeTopics(Set(locationId)))) Replication.grpcEdgeReplication(replicationSettings)(ChargingStation.apply) } + // #edgeReplicaInit /** * Init for running in cloud replica */ + // #replicaInit def init(implicit system: ActorSystem[_]): Replication[Command] = { val replicationSettings = ReplicationSettings[Command](EntityType, R2dbcReplication()) @@ -102,6 +107,7 @@ object ChargingStation { .withEdgeReplication(true) Replication.grpcReplication(replicationSettings)(ChargingStation.apply) } + // #replicaInit def apply( replicatedBehaviors: ReplicatedBehaviors[Command, Event, Option[State]]) @@ -130,11 +136,14 @@ class ChargingStation( handleCommand, handleEvent) // tag with location id so we can replicate only to the right edge node + // #tagging .withTaggerForState { case (None, _) => Set.empty case (Some(state), _) => Set("t:" + state.stationLocationId) } + // #tagging + // #commandHandler private def handleCommand( state: Option[State], command: Command): Effect[Event, Option[State]] = @@ -219,7 +228,9 @@ class ChargingStation( } } + // #commandHandler + // #eventHandler private def handleEvent(state: Option[State], event: Event): Option[State] = { state match { case None => @@ -250,5 +261,6 @@ class ChargingStation( } } + // #eventHandler } diff --git a/samples/grpc/local-drone-control-scala/src/main/scala/local/drones/DroneServiceImpl.scala b/samples/grpc/local-drone-control-scala/src/main/scala/local/drones/DroneServiceImpl.scala index 86c52e65f..63262a692 100644 --- a/samples/grpc/local-drone-control-scala/src/main/scala/local/drones/DroneServiceImpl.scala +++ b/samples/grpc/local-drone-control-scala/src/main/scala/local/drones/DroneServiceImpl.scala @@ -87,6 +87,7 @@ class DroneServiceImpl( .map(_ => Empty.defaultInstance) } + // #charging override def goCharge( in: proto.GoChargeRequest): Future[proto.ChargingResponse] = { logger.info( @@ -109,6 +110,8 @@ class DroneServiceImpl( } convertError(response) } + // #charging + override def completeCharge(in: proto.CompleteChargeRequest) : Future[proto.CompleteChargingResponse] = { diff --git a/samples/grpc/restaurant-drone-deliveries-service-java/src/main/java/central/Main.java b/samples/grpc/restaurant-drone-deliveries-service-java/src/main/java/central/Main.java index 063174788..f6997908b 100644 --- a/samples/grpc/restaurant-drone-deliveries-service-java/src/main/java/central/Main.java +++ b/samples/grpc/restaurant-drone-deliveries-service-java/src/main/java/central/Main.java @@ -50,6 +50,8 @@ private static void init(ActorSystem system) { var deliveryEventsProducerSource = DeliveryEvents.eventProducerSource(system); var droneOverviewService = new DroneOverviewServiceImpl(system, settings); var restaurantDeliveriesService = new RestaurantDeliveriesServiceImpl(system, settings); + + // #replicationEndpoint var chargingStationService = new ChargingStationServiceImpl( settings, @@ -74,6 +76,7 @@ private static void init(ActorSystem system) { pushedEventsDestination, chargingStationReplication.eventProducerPushDestination().get()), system); + // #replicationEndpoint DroneDeliveriesServer.start( system, diff --git a/samples/grpc/restaurant-drone-deliveries-service-scala/src/main/scala/central/Main.scala b/samples/grpc/restaurant-drone-deliveries-service-scala/src/main/scala/central/Main.scala index f45a97d84..86301ba90 100644 --- a/samples/grpc/restaurant-drone-deliveries-service-scala/src/main/scala/central/Main.scala +++ b/samples/grpc/restaurant-drone-deliveries-service-scala/src/main/scala/central/Main.scala @@ -55,6 +55,7 @@ object Main { val restaurantDeliveriesService = new RestaurantDeliveriesServiceImpl(system, settings) + // #replicationEndpoint val chargingStationService = new ChargingStationServiceImpl( chargingStationReplication.entityRefFactory) @@ -71,6 +72,7 @@ object Main { Set( pushedEventsDestination, chargingStationReplication.eventProducerPushDestination.get))(system) + // #replicationEndpoint DroneDeliveriesServer.start( interface, From d6704cdcda178ab45c8b84f0101ffa0fb0b42da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Thu, 14 Dec 2023 11:25:58 +0100 Subject: [PATCH 3/7] Fixes after rebase --- .../main/paradox/guide/5-charging-station.md | 29 ++++++++++--------- .../main/java/charging/ChargingStation.java | 2 ++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/akka-edge-docs/src/main/paradox/guide/5-charging-station.md b/akka-edge-docs/src/main/paradox/guide/5-charging-station.md index 3e160329e..a00dedb25 100644 --- a/akka-edge-docs/src/main/paradox/guide/5-charging-station.md +++ b/akka-edge-docs/src/main/paradox/guide/5-charging-station.md @@ -71,12 +71,15 @@ Scala Java : @@snip [ChargingStation.java](/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java) { #eventHandler } +@@@ note + The charging station is a very limited replicated entity example to keep the guide simple. It doesn't expect any possible conflicts, stations are created, once, in the central cloud and replicated to the edge, updates related to drones currently charging in the station happen at the edge and are replicated to the cloud. Akka replicated event sourcing provides APIs for both CRDTs where conflicts are automatically handled by the data structure and business domain level conflict resolution. For more details about see the @extref[Akka documentation](akka:replicated-eventsourcing.html). +@@@ ### Tagging based on location @@ -89,19 +92,6 @@ Scala Java : @@snip [ChargingStation.java](/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java) { #tagging } -### Additional logic on start - -In case the entity is stopped or entire local-drone-control system is shut down, we also need to schedule completion of -current charging sessions when it completes starting up, this is done with a signal handler for the `RecoveryCompleted` signal -which Akka will send to it every time it has completed starting up. Note how we are using the `ReplicationContext` to only -do this on the replica where the charging was started: - -Scala -: @@snip [ChargingStation.scala](/samples/grpc/local-drone-control-scala/src/main/scala/charging/ChargingStation.scala) { #eventHandler } - -Java -: @@snip [ChargingStation.java](/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java) { #eventHandler } - ### Serialization The state and events of the entity must be serializable because they are written to the datastore, if the local drone control needs to scale out across several nodes to handle traffic, the commands would also be sent between nodes within the Akka cluster. The sample project includes built-in CBOR serialization using the @extref[Akka Serialization Jackson module](akka:serialization-jackson.html). This section describes how serialization is implemented. You do not need to do anything specific to take advantage of CBOR, but this section explains how it is included. @@ -261,4 +251,15 @@ Inspect the state of the charging station in the cloud service to see the chargi ```shell grpcurl -d '{"charging_station_id":"station1"}' -plaintext localhost:8101 charging.ChargingStationService.GetChargingStationState -``` \ No newline at end of file +``` +Inform the station that the drone completed charging: + +```shell +grpcurl -d '{"drone_id":"drone1","charging_station_id":"station1"}' -plaintext 127.0.0.1:8080 local.drones.DroneService.CompleteCharge +``` + +Again query the restaurant-drone-deliveries charge station inspection command to see the set of charging drones changing again: + +```shell +grpcurl -d '{"charging_station_id":"station1"}' -plaintext localhost:8101 charging.ChargingStationService.GetChargingStationState +``` diff --git a/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java b/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java index fa34eccd8..a0fdb9b15 100644 --- a/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java +++ b/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java @@ -319,6 +319,7 @@ private Effect handleCompleteCharging( } // #commandHandler + // #eventHandler @Override public EventHandler eventHandler() { var noStateHandler = @@ -367,6 +368,7 @@ public EventHandler eventHandler() { return noStateHandler.orElse(initializedStateHandler).build(); } + // #eventHandler // #tagging @Override From 5807f71f843459244f1c24415df8d4a2eeb1503c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Thu, 14 Dec 2023 11:34:46 +0100 Subject: [PATCH 4/7] review feedback addressed --- .../main/paradox/guide/1-local-drone-control-service.md | 8 ++++---- .../paradox/guide/2-drone-location-to-cloud-service.md | 4 ++-- .../main/paradox/guide/3-restaurant-deliveries-service.md | 4 ++-- .../paradox/guide/4-local-drone-delivery-selection.md | 6 ++++-- .../src/main/paradox/guide/5-charging-station.md | 6 ++++-- .../src/main/paradox/guide/6-deploying-the-services.md | 8 ++++---- 6 files changed, 20 insertions(+), 16 deletions(-) diff --git a/akka-edge-docs/src/main/paradox/guide/1-local-drone-control-service.md b/akka-edge-docs/src/main/paradox/guide/1-local-drone-control-service.md index 1225195d4..80189b589 100644 --- a/akka-edge-docs/src/main/paradox/guide/1-local-drone-control-service.md +++ b/akka-edge-docs/src/main/paradox/guide/1-local-drone-control-service.md @@ -85,9 +85,9 @@ Java ### Serialization -The state and events of the entity must be serializable because they are written to the datastore, if the local drone control needs to scale out across several nodes to handle traffic, the commands would also be sent between nodes within the Akka cluster. The sample project includes built-in CBOR serialization using the @extref[Akka Serialization Jackson module](akka:serialization-jackson.html). This section describes how serialization is implemented. You do not need to do anything specific to take advantage of CBOR, but this section explains how it is included. +The state and events of the entity must be serializable because they are written to the datastore, if the local drone control needs to scale out across several nodes to handle traffic, the commands would also be sent between nodes within the Akka cluster. -The state, commands and events are marked as `akka.serialization.jackson.CborSerializable` which is configured to use the built-in CBOR serialization. +The state, commands and events are marked with `akka.serialization.jackson.CborSerializable` to use CBOR serialization from @extref[Akka Serialization Jackson module](akka:serialization-jackson.html) for serialization. ### Journal storage @@ -173,8 +173,8 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next steps of the guide: -* Java: https://github.com/akka/akka-projection/tree/main/samples/grpc/local-drone-control-service-java -* Scala: https://github.com/akka/akka-projection/tree/main/samples/grpc/local-drone-control-service-scala +* local-drone-control-service Scala: [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) +* local-drone-control-service Java: [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) @@@ div { .group-scala } diff --git a/akka-edge-docs/src/main/paradox/guide/2-drone-location-to-cloud-service.md b/akka-edge-docs/src/main/paradox/guide/2-drone-location-to-cloud-service.md index 2305acabd..013b5d1e6 100644 --- a/akka-edge-docs/src/main/paradox/guide/2-drone-location-to-cloud-service.md +++ b/akka-edge-docs/src/main/paradox/guide/2-drone-location-to-cloud-service.md @@ -240,8 +240,8 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next steps of the guide: -* Java: https://github.com/akka/akka-projection/tree/main/samples/grpc/restaurant-drone-deliveries-service-java -* Scala: https://github.com/akka/akka-projection/tree/main/samples/grpc/restaurant-drone-deliveries-service-scala +* restaurant-drone-deliveries-service Scala: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) +* restaurant-drone-deliveries-service Java: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) As this service consumes events from the service built in the previous step, start the local-drone-control service first: diff --git a/akka-edge-docs/src/main/paradox/guide/3-restaurant-deliveries-service.md b/akka-edge-docs/src/main/paradox/guide/3-restaurant-deliveries-service.md index 5695f25a1..1aaca39d7 100644 --- a/akka-edge-docs/src/main/paradox/guide/3-restaurant-deliveries-service.md +++ b/akka-edge-docs/src/main/paradox/guide/3-restaurant-deliveries-service.md @@ -73,8 +73,8 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next step of the guide: -* Java: https://github.com/akka/akka-projection/tree/main/samples/grpc/restaurant-drone-deliveries-service-java -* Scala: https://github.com/akka/akka-projection/tree/main/samples/grpc/restaurant-drone-deliveries-service-scala +* restaurant-drone-deliveries-service Scala: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) +* restaurant-drone-deliveries-service Java: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) In this step we created a local entity, so we can try it out by running the restaurant-drone-deliveries-service without any local-drone-control services. diff --git a/akka-edge-docs/src/main/paradox/guide/4-local-drone-delivery-selection.md b/akka-edge-docs/src/main/paradox/guide/4-local-drone-delivery-selection.md index a5ef9ae33..804f737dd 100644 --- a/akka-edge-docs/src/main/paradox/guide/4-local-drone-delivery-selection.md +++ b/akka-edge-docs/src/main/paradox/guide/4-local-drone-delivery-selection.md @@ -163,8 +163,10 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next steps of the guide: -* Java: https://github.com/akka/akka-projection/tree/main/samples/grpc/restaurant-drone-deliveries-service-java -* Scala: https://github.com/akka/akka-projection/tree/main/samples/grpc/restaurant-drone-deliveries-service-scala +* restaurant-drone-deliveries-service Scala: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) +* local-drone-control-service Scala: [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) +* restaurant-drone-deliveries-service Java: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) +* local-drone-control-service Java: [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) As this service consumes events from the service built in the previous step, start the local-drone-control service first: diff --git a/akka-edge-docs/src/main/paradox/guide/5-charging-station.md b/akka-edge-docs/src/main/paradox/guide/5-charging-station.md index a00dedb25..6b87f752e 100644 --- a/akka-edge-docs/src/main/paradox/guide/5-charging-station.md +++ b/akka-edge-docs/src/main/paradox/guide/5-charging-station.md @@ -168,8 +168,10 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next steps of the guide: -* Java: https://github.com/akka/akka-projection/tree/main/samples/grpc/restaurant-drone-deliveries-service-java -* Scala: https://github.com/akka/akka-projection/tree/main/samples/grpc/restaurant-drone-deliveries-service-scala +* restaurant-drone-deliveries-service Scala: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) +* local-drone-control-service Scala: [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) +* restaurant-drone-deliveries-service Java: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) +* local-drone-control-service Java: [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) As this service consumes events from the service built in the previous step, start the local-drone-control service first: diff --git a/akka-edge-docs/src/main/paradox/guide/6-deploying-the-services.md b/akka-edge-docs/src/main/paradox/guide/6-deploying-the-services.md index 4fed2dc35..b17969931 100644 --- a/akka-edge-docs/src/main/paradox/guide/6-deploying-the-services.md +++ b/akka-edge-docs/src/main/paradox/guide/6-deploying-the-services.md @@ -116,8 +116,8 @@ We are now going to deploy the `restaurant-drone-deliveries-service` to the crea This step is for deploying: -* Java: https://github.com/akka/akka-projection/tree/main/samples/grpc/restaurant-drone-deliveries-service-java -* Scala: https://github.com/akka/akka-projection/tree/main/samples/grpc/restaurant-drone-deliveries-service-scala +* restaurant-drone-deliveries-service Scala: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) +* restaurant-drone-deliveries-service Java: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) Build and publish the docker image to docker.io: @@ -216,8 +216,8 @@ kubectl get services This step is for deploying: -* Java: https://github.com/akka/akka-projection/tree/main/samples/grpc/local-drone-control-java -* Scala: https://github.com/akka/akka-projection/tree/main/samples/grpc/local-drone-control-scala +* local-drone-control-service Scala: [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) +* local-drone-control-service Java: [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) ### Local Drone Control Namespace From b3d67f54c1457a18932551952f8181e1245ea449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Thu, 14 Dec 2023 11:43:26 +0100 Subject: [PATCH 5/7] review feedback addressed --- .../paradox/guide/1-local-drone-control-service.md | 4 ++-- .../guide/2-drone-location-to-cloud-service.md | 8 ++++++-- .../paradox/guide/3-restaurant-deliveries-service.md | 4 ++-- .../guide/4-local-drone-delivery-selection.md | 12 +++++++----- .../src/main/paradox/guide/5-charging-station.md | 10 ++++++---- .../main/paradox/guide/6-deploying-the-services.md | 8 ++++---- 6 files changed, 27 insertions(+), 19 deletions(-) diff --git a/akka-edge-docs/src/main/paradox/guide/1-local-drone-control-service.md b/akka-edge-docs/src/main/paradox/guide/1-local-drone-control-service.md index 80189b589..b144c2d4d 100644 --- a/akka-edge-docs/src/main/paradox/guide/1-local-drone-control-service.md +++ b/akka-edge-docs/src/main/paradox/guide/1-local-drone-control-service.md @@ -173,8 +173,8 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next steps of the guide: -* local-drone-control-service Scala: [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) -* local-drone-control-service Java: [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) +* Scala [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) +* Java [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) @@@ div { .group-scala } diff --git a/akka-edge-docs/src/main/paradox/guide/2-drone-location-to-cloud-service.md b/akka-edge-docs/src/main/paradox/guide/2-drone-location-to-cloud-service.md index 013b5d1e6..d5783f57f 100644 --- a/akka-edge-docs/src/main/paradox/guide/2-drone-location-to-cloud-service.md +++ b/akka-edge-docs/src/main/paradox/guide/2-drone-location-to-cloud-service.md @@ -240,8 +240,12 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next steps of the guide: -* restaurant-drone-deliveries-service Scala: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) -* restaurant-drone-deliveries-service Java: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) +* Scala + * [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) + * [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) +* Java + * [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) + * [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) As this service consumes events from the service built in the previous step, start the local-drone-control service first: diff --git a/akka-edge-docs/src/main/paradox/guide/3-restaurant-deliveries-service.md b/akka-edge-docs/src/main/paradox/guide/3-restaurant-deliveries-service.md index 1aaca39d7..60634a3cf 100644 --- a/akka-edge-docs/src/main/paradox/guide/3-restaurant-deliveries-service.md +++ b/akka-edge-docs/src/main/paradox/guide/3-restaurant-deliveries-service.md @@ -73,8 +73,8 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next step of the guide: -* restaurant-drone-deliveries-service Scala: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) -* restaurant-drone-deliveries-service Java: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) +* Scala [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) +* Java [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) In this step we created a local entity, so we can try it out by running the restaurant-drone-deliveries-service without any local-drone-control services. diff --git a/akka-edge-docs/src/main/paradox/guide/4-local-drone-delivery-selection.md b/akka-edge-docs/src/main/paradox/guide/4-local-drone-delivery-selection.md index 804f737dd..03fbe3578 100644 --- a/akka-edge-docs/src/main/paradox/guide/4-local-drone-delivery-selection.md +++ b/akka-edge-docs/src/main/paradox/guide/4-local-drone-delivery-selection.md @@ -163,11 +163,13 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next steps of the guide: -* restaurant-drone-deliveries-service Scala: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) -* local-drone-control-service Scala: [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) -* restaurant-drone-deliveries-service Java: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) -* local-drone-control-service Java: [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) - +* Scala + * [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) + * [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) +* Java + * [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) + * [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) + As this service consumes events from the service built in the previous step, start the local-drone-control service first: @@@ div { .group-scala } diff --git a/akka-edge-docs/src/main/paradox/guide/5-charging-station.md b/akka-edge-docs/src/main/paradox/guide/5-charging-station.md index 6b87f752e..dfffd869c 100644 --- a/akka-edge-docs/src/main/paradox/guide/5-charging-station.md +++ b/akka-edge-docs/src/main/paradox/guide/5-charging-station.md @@ -168,10 +168,12 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next steps of the guide: -* restaurant-drone-deliveries-service Scala: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) -* local-drone-control-service Scala: [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) -* restaurant-drone-deliveries-service Java: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) -* local-drone-control-service Java: [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) +* Scala + * [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) + * [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) +* Java + * [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) + * [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) As this service consumes events from the service built in the previous step, start the local-drone-control service first: diff --git a/akka-edge-docs/src/main/paradox/guide/6-deploying-the-services.md b/akka-edge-docs/src/main/paradox/guide/6-deploying-the-services.md index b17969931..de0c0a2f0 100644 --- a/akka-edge-docs/src/main/paradox/guide/6-deploying-the-services.md +++ b/akka-edge-docs/src/main/paradox/guide/6-deploying-the-services.md @@ -116,8 +116,8 @@ We are now going to deploy the `restaurant-drone-deliveries-service` to the crea This step is for deploying: -* restaurant-drone-deliveries-service Scala: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) -* restaurant-drone-deliveries-service Java: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) +* Scala: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) +* Java: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) Build and publish the docker image to docker.io: @@ -216,8 +216,8 @@ kubectl get services This step is for deploying: -* local-drone-control-service Scala: [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) -* local-drone-control-service Java: [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) +* Scala: [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) +* Java: [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) ### Local Drone Control Namespace From 9c4a990b243e02d705576ea16b53790963a208f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Fri, 15 Dec 2023 14:55:54 +0100 Subject: [PATCH 6/7] Identical charger actors --- .../main/java/charging/ChargingStation.java | 19 ++++++++++++++++--- .../main/scala/charging/ChargingStation.scala | 18 ++++++++++++++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/samples/grpc/restaurant-drone-deliveries-service-java/src/main/java/charging/ChargingStation.java b/samples/grpc/restaurant-drone-deliveries-service-java/src/main/java/charging/ChargingStation.java index 3839a7eac..a0fdb9b15 100644 --- a/samples/grpc/restaurant-drone-deliveries-service-java/src/main/java/charging/ChargingStation.java +++ b/samples/grpc/restaurant-drone-deliveries-service-java/src/main/java/charging/ChargingStation.java @@ -26,7 +26,7 @@ public class ChargingStation extends ReplicatedEventSourcedBehavior< ChargingStation.Command, ChargingStation.Event, ChargingStation.State> { - // commands and replies + // #commands public interface Command extends CborSerializable {} public static final class Create implements Command { @@ -81,8 +81,9 @@ public CompleteCharging(String droneId, ActorRef> replyTo) { this.replyTo = replyTo; } } + // #commands - // events + // #events public interface Event extends CborSerializable {} public static final class Created implements Event { @@ -125,7 +126,9 @@ public ChargingDrone(String droneId, Instant expectedComplete, String replicaId) this.replicaId = replicaId; } } + // #events + // #state public static final class State implements CborSerializable { public final int chargingSlots; public final Set dronesCharging; @@ -137,6 +140,7 @@ public State(int chargingSlots, Set dronesCharging, String statio this.stationLocationId = stationLocationId; } } + // #state public static final String ENTITY_TYPE = "charging-station"; @@ -146,6 +150,7 @@ public State(int chargingSlots, Set dronesCharging, String statio * Init for running in edge node, this is the only difference from the ChargingStation in * restaurant-deliveries-service */ + // #edgeReplicaInit public static EdgeReplication initEdge(ActorSystem system, String locationId) { var replicationSettings = ReplicationSettings.create( @@ -158,8 +163,10 @@ public static EdgeReplication initEdge(ActorSystem system, String lo new ConsumerFilter.IncludeTopics(Set.of(locationId)))); return Replication.grpcEdgeReplication(replicationSettings, ChargingStation::create, system); } + // #edgeReplicaInit /** Init for running in cloud replica */ + // #replicaInit public static Replication init(ActorSystem system) { var replicationSettings = ReplicationSettings.create( @@ -168,6 +175,7 @@ public static Replication init(ActorSystem system) { .withEdgeReplication(true); return Replication.grpcReplication(replicationSettings, ChargingStation::create, system); } + // #replicaInit public static Behavior create( ReplicatedBehaviors replicatedBehaviors) { @@ -194,9 +202,9 @@ public State emptyState() { return null; } + // #commandHandler @Override public CommandHandler commandHandler() { - var noStateHandler = newCommandHandlerBuilder() .forNullState() @@ -309,7 +317,9 @@ private Effect handleCompleteCharging( StatusReply.error( "Drone " + completeCharging.droneId + " is not currently charging.")); } + // #commandHandler + // #eventHandler @Override public EventHandler eventHandler() { var noStateHandler = @@ -358,10 +368,13 @@ public EventHandler eventHandler() { return noStateHandler.orElse(initializedStateHandler).build(); } + // #eventHandler + // #tagging @Override public Set tagsFor(State state, Event event) { if (state == null) return Set.of(); else return Set.of("t:" + state.stationLocationId); } + // #tagging } diff --git a/samples/grpc/restaurant-drone-deliveries-service-scala/src/main/scala/charging/ChargingStation.scala b/samples/grpc/restaurant-drone-deliveries-service-scala/src/main/scala/charging/ChargingStation.scala index 36ac28fb0..cb325f004 100644 --- a/samples/grpc/restaurant-drone-deliveries-service-scala/src/main/scala/charging/ChargingStation.scala +++ b/samples/grpc/restaurant-drone-deliveries-service-scala/src/main/scala/charging/ChargingStation.scala @@ -27,7 +27,7 @@ import scala.concurrent.duration._ object ChargingStation { - // commands and replies + // #commands sealed trait Command extends CborSerializable case class Create( locationId: String, @@ -49,8 +49,9 @@ object ChargingStation { droneId: String, reply: ActorRef[StatusReply[Done]]) extends Command + // #commands - // events + // #events sealed trait Event extends CborSerializable case class Created(locationId: String, chargingSlots: Int) extends Event case class ChargingStarted(droneId: String, expectedComplete: Instant) @@ -58,7 +59,9 @@ object ChargingStation { with StartChargingResponse case class ChargingCompleted(droneId: String) extends Event + // #events + // #state case class ChargingDrone( droneId: String, expectedComplete: Instant, @@ -68,6 +71,7 @@ object ChargingStation { dronesCharging: Set[ChargingDrone], stationLocationId: String) extends CborSerializable + // #state val EntityType = "charging-station" @@ -77,6 +81,7 @@ object ChargingStation { * Init for running in edge node, this is the only difference from the ChargingStation * in restaurant-deliveries-service */ + // #edgeReplicaInit def initEdge(locationId: String)( implicit system: ActorSystem[_]): EdgeReplication[Command] = { val replicationSettings = @@ -89,10 +94,12 @@ object ChargingStation { ConsumerFilter.IncludeTopics(Set(locationId)))) Replication.grpcEdgeReplication(replicationSettings)(ChargingStation.apply) } + // #edgeReplicaInit /** * Init for running in cloud replica */ + // #replicaInit def init(implicit system: ActorSystem[_]): Replication[Command] = { val replicationSettings = ReplicationSettings[Command](EntityType, R2dbcReplication()) @@ -100,6 +107,7 @@ object ChargingStation { .withEdgeReplication(true) Replication.grpcReplication(replicationSettings)(ChargingStation.apply) } + // #replicaInit def apply( replicatedBehaviors: ReplicatedBehaviors[Command, Event, Option[State]]) @@ -128,11 +136,14 @@ class ChargingStation( handleCommand, handleEvent) // tag with location id so we can replicate only to the right edge node + // #tagging .withTaggerForState { case (None, _) => Set.empty case (Some(state), _) => Set("t:" + state.stationLocationId) } + // #tagging + // #commandHandler private def handleCommand( state: Option[State], command: Command): Effect[Event, Option[State]] = @@ -217,7 +228,9 @@ class ChargingStation( } } + // #commandHandler + // #eventHandler private def handleEvent(state: Option[State], event: Event): Option[State] = { state match { case None => @@ -248,5 +261,6 @@ class ChargingStation( } } + // #eventHandler } From eba07881d6272dcaf639a463e3f6d7d2e51c5095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Mon, 18 Dec 2023 13:44:17 +0100 Subject: [PATCH 7/7] review feedback addressed --- .../guide/1-local-drone-control-service.md | 4 ++-- .../2-drone-location-to-cloud-service.md | 8 ++----- .../guide/3-restaurant-deliveries-service.md | 4 ++-- .../guide/4-local-drone-delivery-selection.md | 8 ++----- .../main/paradox/guide/5-charging-station.md | 22 +++++-------------- .../paradox/guide/6-deploying-the-services.md | 12 +++++----- .../scala/local/drones/DroneServiceImpl.scala | 3 +-- 7 files changed, 21 insertions(+), 40 deletions(-) diff --git a/akka-edge-docs/src/main/paradox/guide/1-local-drone-control-service.md b/akka-edge-docs/src/main/paradox/guide/1-local-drone-control-service.md index b144c2d4d..fcf6b5a41 100644 --- a/akka-edge-docs/src/main/paradox/guide/1-local-drone-control-service.md +++ b/akka-edge-docs/src/main/paradox/guide/1-local-drone-control-service.md @@ -173,8 +173,8 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next steps of the guide: -* Scala [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) -* Java [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) +* Scala [drone-scala.zip](../attachments/drone-scala.zip) +* Java [drone-java.zip](../attachments/drone-java.zip) @@@ div { .group-scala } diff --git a/akka-edge-docs/src/main/paradox/guide/2-drone-location-to-cloud-service.md b/akka-edge-docs/src/main/paradox/guide/2-drone-location-to-cloud-service.md index d5783f57f..651dab993 100644 --- a/akka-edge-docs/src/main/paradox/guide/2-drone-location-to-cloud-service.md +++ b/akka-edge-docs/src/main/paradox/guide/2-drone-location-to-cloud-service.md @@ -240,12 +240,8 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next steps of the guide: -* Scala - * [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) - * [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) -* Java - * [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) - * [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) +* Scala [drone-scala.zip](../attachments/drone-scala.zip) +* Java [drone-java.zip](../attachments/drone-java.zip) As this service consumes events from the service built in the previous step, start the local-drone-control service first: diff --git a/akka-edge-docs/src/main/paradox/guide/3-restaurant-deliveries-service.md b/akka-edge-docs/src/main/paradox/guide/3-restaurant-deliveries-service.md index 60634a3cf..8ff3d8ed5 100644 --- a/akka-edge-docs/src/main/paradox/guide/3-restaurant-deliveries-service.md +++ b/akka-edge-docs/src/main/paradox/guide/3-restaurant-deliveries-service.md @@ -73,8 +73,8 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next step of the guide: -* Scala [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) -* Java [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) +* Scala [drone-scala.zip](../attachments/drone-scala.zip) +* Java [drone-java.zip](../attachments/drone-java.zip) In this step we created a local entity, so we can try it out by running the restaurant-drone-deliveries-service without any local-drone-control services. diff --git a/akka-edge-docs/src/main/paradox/guide/4-local-drone-delivery-selection.md b/akka-edge-docs/src/main/paradox/guide/4-local-drone-delivery-selection.md index 03fbe3578..7fd5f530b 100644 --- a/akka-edge-docs/src/main/paradox/guide/4-local-drone-delivery-selection.md +++ b/akka-edge-docs/src/main/paradox/guide/4-local-drone-delivery-selection.md @@ -163,12 +163,8 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next steps of the guide: -* Scala - * [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) - * [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) -* Java - * [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) - * [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) +* Scala [drone-scala.zip](../attachments/drone-scala.zip) +* Java [drone-java.zip](../attachments/drone-java.zip) As this service consumes events from the service built in the previous step, start the local-drone-control service first: diff --git a/akka-edge-docs/src/main/paradox/guide/5-charging-station.md b/akka-edge-docs/src/main/paradox/guide/5-charging-station.md index dfffd869c..278209844 100644 --- a/akka-edge-docs/src/main/paradox/guide/5-charging-station.md +++ b/akka-edge-docs/src/main/paradox/guide/5-charging-station.md @@ -47,8 +47,7 @@ The command handler is in fact two separate handlers, one for when the entity is the `Create` command, and one that is used to handle commands once the station has been initialized. The `StartCharging` command is the only one that requires more complicated logic: if a slot is free, persist a `ChargingStarted` -event and tell the drone when charging will be done. A timer is also set, to send a `CompleteCharging` to itself once the -charging time has passed. If all charging slots are busy the reply will instead be when the first slot will +event and tell the drone when charging will be done. If all charging slots are busy the reply will instead be when the first slot will be free again and the drone can come back and try charge again. Scala @@ -92,12 +91,6 @@ Scala Java : @@snip [ChargingStation.java](/samples/grpc/local-drone-control-java/src/main/java/charging/ChargingStation.java) { #tagging } -### Serialization - -The state and events of the entity must be serializable because they are written to the datastore, if the local drone control needs to scale out across several nodes to handle traffic, the commands would also be sent between nodes within the Akka cluster. The sample project includes built-in CBOR serialization using the @extref[Akka Serialization Jackson module](akka:serialization-jackson.html). This section describes how serialization is implemented. You do not need to do anything specific to take advantage of CBOR, but this section explains how it is included. - -The state, commands and events are marked as `akka.serialization.jackson.CborSerializable` which is configured to use the built-in CBOR serialization. - ### Setting up replication for the charging station Setup for the cloud replica and the edge node differs slightly. @@ -154,8 +147,9 @@ Java ## Interacting with the charging station at the edge -The local-drone-control service does not contain the full gRPC API for interaction but instead has a single `goCharge` method -in the drone gRPC service: +The local-drone-control service does not contain the full gRPC API for creating and inspecting charging stations but +instead two methods in the drone gRPC service `goCharge` to initiate charging of a drone if possible and `completeCharge` +to complete a charging session for a given drone: Scala : @@snip [DroneServiceImpl.scala](/samples/grpc/local-drone-control-scala/src/main/scala/local/drones/DroneServiceImpl.scala) { #charging } @@ -168,12 +162,8 @@ Java The complete sample can be downloaded from GitHub, but note that it also includes the next steps of the guide: -* Scala - * [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) - * [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) -* Java - * [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) - * [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) +* Scala [drone-scala.zip](../attachments/drone-scala.zip) +* Java [drone-java.zip](../attachments/drone-java.zip) As this service consumes events from the service built in the previous step, start the local-drone-control service first: diff --git a/akka-edge-docs/src/main/paradox/guide/6-deploying-the-services.md b/akka-edge-docs/src/main/paradox/guide/6-deploying-the-services.md index de0c0a2f0..c4e90f8ee 100644 --- a/akka-edge-docs/src/main/paradox/guide/6-deploying-the-services.md +++ b/akka-edge-docs/src/main/paradox/guide/6-deploying-the-services.md @@ -114,10 +114,10 @@ SQL We are now going to deploy the `restaurant-drone-deliveries-service` to the created kubernetes cluster in `us-east-2`. -This step is for deploying: +This step is for deploying the restaurant-deliveries-service project, full sources can be downloaded from: -* Scala: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-scala.zip) -* Java: [restaurant-drone-deliveries-service.zip](../attachments/restaurant-drone-deliveries-service-java.zip) +* Scala [drone-scala.zip](../attachments/drone-scala.zip) +* Java [drone-java.zip](../attachments/drone-java.zip) Build and publish the docker image to docker.io: @@ -214,10 +214,10 @@ kubectl get services ## Deploy local drone control instances -This step is for deploying: +This step is for deploying the local-drone-control project, full sources can be downloaded from: -* Scala: [local-drone-control-service.zip](../attachments/local-drone-control-service-scala.zip) -* Java: [local-drone-control-service.zip](../attachments/local-drone-control-service-java.zip) +* Scala [drone-scala.zip](../attachments/drone-scala.zip) +* Java [drone-java.zip](../attachments/drone-java.zip) ### Local Drone Control Namespace diff --git a/samples/grpc/local-drone-control-scala/src/main/scala/local/drones/DroneServiceImpl.scala b/samples/grpc/local-drone-control-scala/src/main/scala/local/drones/DroneServiceImpl.scala index 63262a692..99bbed4ec 100644 --- a/samples/grpc/local-drone-control-scala/src/main/scala/local/drones/DroneServiceImpl.scala +++ b/samples/grpc/local-drone-control-scala/src/main/scala/local/drones/DroneServiceImpl.scala @@ -110,8 +110,6 @@ class DroneServiceImpl( } convertError(response) } - // #charging - override def completeCharge(in: proto.CompleteChargeRequest) : Future[proto.CompleteChargingResponse] = { @@ -127,6 +125,7 @@ class DroneServiceImpl( convertError(response) } + // #charging private def convertError[T](response: Future[T]): Future[T] = { response.recoverWith {