getRootEventType();
27 |
28 | String asyncPersistentEntityDispatcherName();
29 |
30 | String pipeDispatcherName();
31 |
32 | long scheduledAsyncEntityActionTimeout();
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/AsyncResult.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing.persistance;
2 |
3 | import java.io.Serializable;
4 |
5 | import com.spring.akka.eventsourcing.persistance.eventsourcing.actions.Persist;
6 |
7 | import lombok.Builder;
8 | import lombok.Value;
9 |
10 | /**
11 | * @param the event type
12 | *
13 | * the async result class that contain the async persist action and if exist the error response
14 | */
15 | @Builder
16 | @Value
17 | public class AsyncResult implements Serializable {
18 | private Persist persist;
19 | private ErrorResponse errorResponse;
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/ErrorResponse.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing.persistance;
2 |
3 | import java.io.Serializable;
4 |
5 | import lombok.Builder;
6 | import lombok.Value;
7 |
8 | /**
9 | * Generic Error response class
10 | */
11 | @Builder
12 | @Value
13 | public class ErrorResponse implements Serializable {
14 | private String errorMsg;
15 | private String errorCode;
16 | private String exceptionMsg;
17 | }
18 |
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/ResumeProcessing.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing.persistance;
2 |
3 | import java.io.Serializable;
4 |
5 | /**
6 | * the message trigger type to inform the persistent actor to resume processing and stop stashing messages
7 | */
8 | public class ResumeProcessing implements Serializable {
9 | }
10 |
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/ExecutionFlow.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing.persistance.eventsourcing;
2 |
3 | import java.util.Map;
4 | import java.util.concurrent.CompletionStage;
5 | import java.util.function.BiConsumer;
6 | import java.util.function.BiFunction;
7 |
8 | import com.spring.akka.eventsourcing.persistance.AsyncResult;
9 | import com.spring.akka.eventsourcing.persistance.eventsourcing.actions.Persist;
10 |
11 | import lombok.Builder;
12 | import lombok.Data;
13 | import lombok.Singular;
14 |
15 | /**
16 | * @param root command class type
17 | * @param root event class
18 | * @param root state class
19 | *
20 | * builder class for the execution flow of the aggregate entity
21 | */
22 | @Builder
23 | @Data
24 | public class ExecutionFlow {
25 |
26 | private S state;
27 | @Singular("onCommand")
28 | private Map, TriFunction>> onCommand;
29 | @Singular("asyncOnCommand")
30 | private Map, TriFunction>>> asyncOnCommand;
31 | @Singular("onEvent")
32 | private Map, BiFunction> onEvent;
33 |
34 |
35 | public static class ExecutionFlowBuilder {
36 |
37 | /*
38 | * Register a read-only command handler for a given command class. A read-only command
39 | * handler does not persist events (i.e. it does not change state) but it may perform side
40 | * effects, such as replying to the request. Replies are sent with the `reply` method of the
41 | * context that is passed to the command handler function.
42 | */
43 | public ExecutionFlowBuilder onReadOnlyCommand(Class extends C> command, BiConsumer handler) {
44 |
45 | onCommand(command, (cmd, flowContext, state) -> {
46 | handler.accept(cmd, flowContext);
47 | return flowContext.done();
48 | });
49 | return this;
50 | }
51 |
52 |
53 | }
54 |
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/FlowContext.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing.persistance.eventsourcing;
2 |
3 | import java.util.List;
4 | import java.util.function.Consumer;
5 |
6 | import com.spring.akka.eventsourcing.persistance.eventsourcing.actions.Persist;
7 | import com.spring.akka.eventsourcing.persistance.eventsourcing.actions.PersistAll;
8 | import com.spring.akka.eventsourcing.persistance.eventsourcing.actions.PersistNone;
9 | import com.spring.akka.eventsourcing.persistance.eventsourcing.actions.PersistOne;
10 |
11 | public abstract class FlowContext extends ReadOnlyFlowContext {
12 | /**
13 | * A command handler may return this `Persist` type to define
14 | * that one event is to be persisted.
15 | */
16 | public Persist thenPersist(E event) {
17 | return PersistOne.builder().event(event).afterPersist(null).build();
18 | }
19 |
20 | /**
21 | * A command handler may return this `Persist` type to define
22 | * that one event is to be persisted. External side effects can be
23 | * performed after successful persist in the `afterPersist` function.
24 | */
25 | public Persist thenPersist(E event, Consumer afterPersist) {
26 |
27 | return PersistOne.builder().event(event).afterPersist(afterPersist).build();
28 | }
29 |
30 | /**
31 | * A command handler may return this `Persist`type to define
32 | * that several events are to be persisted.
33 | */
34 | public Persist thenPersistAll(List events) {
35 | return PersistAll.builder().afterPersist(null).events(events).build();
36 | }
37 |
38 | /**
39 | * A command handler may return this `Persist` type to define
40 | * that several events are to be persisted. External side effects can be
41 | * performed after successful persist in the `afterPersist` function.
42 | * `afterPersist` is invoked once when all events have been persisted
43 | * successfully.
44 | */
45 | public Persist thenPersistAll(List events, Consumer> afterPersist) {
46 | return PersistAll.builder().afterPersist(afterPersist).events(events).build();
47 | }
48 |
49 |
50 | /**
51 | * A command handler may return this `Persist` type to define
52 | * that no events are to be persisted.
53 | */
54 | public Persist done() {
55 | return PersistNone.builder().build();
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/PersistentEntityBroker.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing.persistance.eventsourcing;
2 |
3 | import static akka.actor.Props.create;
4 |
5 | import java.util.Collections;
6 | import java.util.HashMap;
7 | import java.util.List;
8 | import java.util.Map;
9 |
10 | import javax.annotation.PostConstruct;
11 |
12 | import org.springframework.beans.factory.annotation.Autowired;
13 | import org.springframework.stereotype.Service;
14 |
15 | import com.spring.akka.eventsourcing.config.PersistentEntityProperties;
16 |
17 | import akka.actor.ActorRef;
18 | import akka.actor.ActorSystem;
19 |
20 | /**
21 | * generic spring managed bean persistent entity broker that abstract cluster sharding
22 | * for the enabled entity managed spring beans persistent actors
23 | */
24 | @Service
25 | public class PersistentEntityBroker {
26 |
27 | private final ActorSystem actorSystem;
28 |
29 | private final List persistentEntityProperties;
30 |
31 | private Map, ActorRef> shardingRegistery;
32 |
33 |
34 | @Autowired
35 | public PersistentEntityBroker(ActorSystem actorSystem, List persistentEntityProperties) {
36 | this.actorSystem = actorSystem;
37 | this.persistentEntityProperties = persistentEntityProperties;
38 | }
39 |
40 | /**
41 | * auto discover the different configuration of the different persistent entity actors and create a lookup of them for the broker
42 | */
43 | @PostConstruct
44 | public void init() {
45 |
46 | Map, ActorRef> initMap = new HashMap<>();
47 |
48 | persistentEntityProperties.forEach(persistentEntityConfig -> initMap.put(persistentEntityConfig.getEntityClass(),
49 | PersistentEntitySharding.of(create(persistentEntityConfig.getEntityClass(), persistentEntityConfig),
50 | persistentEntityConfig.persistenceIdPrefix(), persistentEntityConfig.persistenceIdPostfix(),
51 | persistentEntityConfig.numberOfShards()).shardRegion(actorSystem)));
52 |
53 | shardingRegistery = Collections.unmodifiableMap(initMap);
54 |
55 | }
56 |
57 | /**
58 | * @param entityClass the persistent entity class to get the cluster sharding for
59 | * @return the found actor ref based into the sharding registery lookup
60 | */
61 | public ActorRef findPersistentEntity(Class extends PersistentEntity> entityClass) {
62 |
63 | final ActorRef sharedRegionPersistentActor = shardingRegistery.get(entityClass);
64 | if (sharedRegionPersistentActor == null) {
65 | throw new IllegalStateException("no persistent entity configuration(PersistentEntityProperties) has been define for that entity class " + entityClass.getSimpleName());
66 | }
67 | return sharedRegionPersistentActor;
68 |
69 |
70 | }
71 |
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/PersistentEntitySharding.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing.persistance.eventsourcing;
2 |
3 | import java.util.function.Function;
4 |
5 | import akka.actor.ActorRef;
6 | import akka.actor.ActorSystem;
7 | import akka.actor.Props;
8 | import akka.cluster.sharding.ClusterSharding;
9 | import akka.cluster.sharding.ClusterShardingSettings;
10 | import akka.cluster.sharding.ShardRegion;
11 | import akka.cluster.sharding.ShardRegion.MessageExtractor;
12 | import akka.persistence.AbstractPersistentActor;
13 |
14 | /**
15 | * Base class for setting up sharding of persistent actors that:
16 | *
17 | * - Have their persistenceId conform to [prefix] + "_" + [id], e.g. "doc_249e1098-1cd9-4ffe-a494-73a430983590"
18 | * - Respond to a specific base class of commands
19 | * - Send the commands unchanged from the SharedRegion router onto the persistent actors themselves
20 | *
21 | * @param Base class of commands that the actor will respond to
22 | */
23 |
24 | public class PersistentEntitySharding {
25 | private final Props props;
26 | private final String persistenceIdPrefix;
27 | private final Function persistenceIdPostfix;
28 | private final int numberOfShards;
29 | private final MessageExtractor messageExtractor = new MessageExtractor() {
30 | @Override
31 | public String entityId(Object command) {
32 | return getEntityId(command);
33 | }
34 |
35 | @Override
36 | public String shardId(Object command) {
37 | if (command instanceof ShardRegion.StartEntity) {
38 | ShardRegion.StartEntity startEntity = (ShardRegion.StartEntity) command;
39 | return getShardId(startEntity.entityId());
40 | } else {
41 | return getShardId(getEntityId(command));
42 | }
43 |
44 | }
45 |
46 | @Override
47 | public Object entityMessage(Object command) {
48 | // we don't need need to unwrap messages sent to the router, they can just be
49 | // forwarded to the target persistent actor directly.
50 | return command;
51 | }
52 | };
53 |
54 | protected PersistentEntitySharding(Props props, String persistenceIdPrefix, Function entityIdForCommand, int numberOfShards) {
55 | this.props = props;
56 | this.persistenceIdPrefix = persistenceIdPrefix;
57 | this.persistenceIdPostfix = entityIdForCommand;
58 | this.numberOfShards = numberOfShards;
59 | }
60 |
61 | /**
62 | * Creates a PersistentEntitySharding for an actor that is created according to [props]. The actor must be a subclass of {@link AbstractPersistentActor}.
63 | * Entities will be sharded onto 256 shards.
64 | *
65 | * @param persistenceIdPrefix Fixed prefix for each persistence id. This is typically the name of your aggregate root, e.g. "document" or "user".
66 | * @param persistenceIdPostfix Function that returns the last part of the persistence id that a command is routed to. This typically is the real ID of your entity, or UUID.
67 | */
68 | public static PersistentEntitySharding of(Props props, String persistenceIdPrefix, Function persistenceIdPostfix) {
69 | return new PersistentEntitySharding<>(props, persistenceIdPrefix, persistenceIdPostfix, 256);
70 | }
71 |
72 | /**
73 | * Creates a PersistentEntitySharding for an actor that is created according to [props]. The actor must be a subclass of {@link AbstractPersistentActor}.
74 | *
75 | * @param numberOfShards Number of shards to divide all entity/persistence ids into. This can not be changed after the first run.
76 | * @param persistenceIdPrefix Fixed prefix for each persistence id. This is typically the name of your aggregate root, e.g. "document" or "user".
77 | * @param persistenceIdPostfix Function that returns the last part of the persistence id that a command is routed to. This typically is the real ID of your entity, or UUID.
78 | */
79 | public static PersistentEntitySharding of(Props props, String persistenceIdPrefix, Function persistenceIdPostfix, int numberOfShards) {
80 | return new PersistentEntitySharding<>(props, persistenceIdPrefix, persistenceIdPostfix, numberOfShards);
81 | }
82 |
83 | /**
84 | * Starts the cluster router (ShardRegion) for this persistent actor type on the given actor system,
85 | * and returns its ActorRef. If it's already running, just returns the ActorRef.
86 | */
87 | public ActorRef shardRegion(ActorSystem system) {
88 | return ClusterSharding.get(system).start(
89 | persistenceIdPrefix,
90 | props,
91 | ClusterShardingSettings.create(system),
92 | messageExtractor);
93 | }
94 |
95 | /**
96 | * Returns the postfix part of a generated persistence ID.
97 | *
98 | * @param persistenceId A persistenceId of an actor that was spawned by sending a command to it through the
99 | * ActorRef returned by {@link #shardRegion}.
100 | */
101 | public String getPersistenceIdPostfix(String persistenceId) {
102 | return persistenceId.substring(persistenceIdPrefix.length() + 1);
103 | }
104 |
105 | /**
106 | * Returns the entityId (=persistenceId) to which the given command should be routed
107 | */
108 | @SuppressWarnings("unchecked")
109 | public String getEntityId(Object command) {
110 | return persistenceIdPrefix + "_" + persistenceIdPostfix.apply((C) command);
111 |
112 | }
113 |
114 | /**
115 | * Returns the shard on which the given entityId should be placed
116 | */
117 | public String getShardId(String entityId) {
118 | return String.valueOf(entityId.hashCode() % numberOfShards);
119 | }
120 |
121 |
122 | }
123 |
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/ReadOnlyFlowContext.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing.persistance.eventsourcing;
2 |
3 | public abstract class ReadOnlyFlowContext {
4 |
5 | /**
6 | * Send reply to a command. The type `R` must be the type defined by
7 | * the command.
8 | */
9 | public abstract void reply(R msg);
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/TriFunction.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing.persistance.eventsourcing;
2 |
3 | import java.util.Objects;
4 | import java.util.function.Function;
5 |
6 | /**
7 | * @author romeh
8 | * Tri function support
9 | */
10 | @FunctionalInterface
11 | public interface TriFunction {
12 |
13 | R apply(A a, B b, C c);
14 |
15 | default TriFunction andThen(
16 | Function super R, ? extends V> after) {
17 | Objects.requireNonNull(after);
18 | return (A a, B b, C c) -> after.apply(apply(a, b, c));
19 | }
20 | }
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/actions/Persist.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing.persistance.eventsourcing.actions;
2 |
3 | import java.io.Serializable;
4 | import java.util.function.Consumer;
5 |
6 | import lombok.AllArgsConstructor;
7 | import lombok.Getter;
8 | import lombok.NoArgsConstructor;
9 | import lombok.Setter;
10 |
11 |
12 | /**
13 | * main class for the action result that need to be done from the command context after executing the command handler
14 | *
15 | * @param the event type
16 | */
17 | @AllArgsConstructor
18 | @NoArgsConstructor
19 | @Getter
20 | @Setter
21 | public class Persist implements Serializable {
22 |
23 | private E event;
24 | private Consumer afterPersist;
25 | }
26 |
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/actions/PersistAll.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing.persistance.eventsourcing.actions;
2 |
3 |
4 | import java.util.List;
5 | import java.util.function.Consumer;
6 |
7 | import lombok.Builder;
8 | import lombok.Getter;
9 |
10 | /**
11 | * main class for the action result that need to be done from the command context after executing the command handler in case of many events to persist
12 | *
13 | * @param the event type
14 | */
15 | public class PersistAll extends Persist {
16 | @Getter
17 | private List events;
18 | @Getter
19 | private Consumer> afterPersistAll;
20 |
21 | @Builder
22 | private PersistAll(Consumer> afterPersist, List events) {
23 | super(null, null);
24 | this.events = events;
25 | this.afterPersistAll = afterPersist;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/actions/PersistNone.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing.persistance.eventsourcing.actions;
2 |
3 | import lombok.Builder;
4 |
5 | /**
6 | * simply it mean nothing to be persisted as an event which is the case of readOnly command handlers
7 | */
8 | @Builder
9 | public class PersistNone extends Persist {
10 |
11 | public PersistNone() {
12 | super(null, null);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/actions/PersistOne.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing.persistance.eventsourcing.actions;
2 |
3 | import java.util.function.Consumer;
4 |
5 | import lombok.Builder;
6 |
7 | /**
8 | * persist one event as a result of command handler execution and check if there is any after persist logic is needed.
9 | *
10 | * @param the event type
11 | */
12 |
13 | public class PersistOne extends Persist {
14 | @Builder
15 | private PersistOne(E event, Consumer afterPersist) {
16 | super(event, afterPersist);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/annotations/PersistentActor.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing.persistance.eventsourcing.annotations;
2 |
3 |
4 | import org.springframework.beans.factory.config.ConfigurableBeanFactory;
5 | import org.springframework.context.annotation.Scope;
6 | import org.springframework.stereotype.Component;
7 |
8 | /**
9 | * a generic annotation to mark custom actor class as a managed spring bean
10 | */
11 | @Component
12 | @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
13 | public @interface PersistentActor {
14 | }
15 |
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/resources/META-INF/spring.factories:
--------------------------------------------------------------------------------
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
2 | com.spring.akka.eventsourcing.config.AkkaAutoConfiguration
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/main/resources/reference.conf:
--------------------------------------------------------------------------------
1 | internal.blocking-io-dispatcher {
2 | type = Dispatcher
3 | executor = "thread-pool-executor"
4 | thread-pool-executor {
5 | fixed-pool-size = 8
6 | }
7 | throughput = 1
8 | }
9 | internal-thread-pool-dispatcher {
10 | # Dispatcher is the name of the event-based dispatcher
11 | type = Dispatcher
12 | # What kind of ExecutionService to use
13 | executor = "thread-pool-executor"
14 | # Configuration for the thread pool
15 | thread-pool-executor {
16 | # minimum number of threads to cap factor-based core number to
17 | core-pool-size-min = 4
18 | # No of core threads ... ceil(available processors * factor)
19 | core-pool-size-factor = 2.0
20 | # maximum number of threads to cap factor-based number to
21 | core-pool-size-max = 16
22 |
23 | # Minimum number of threads to cap factor-based max number to
24 | # (if using a bounded task queue)
25 | max-pool-size-min = 8
26 |
27 | # Max no of threads (if using a bounded task queue) is determined by
28 | # calculating: ceil(available processors * factor)
29 | max-pool-size-factor = 2.0
30 |
31 | # Specifies the bounded capacity of the task queue (< 1 == unbounded)
32 | task-queue-size = -1
33 |
34 | # Specifies which type of task queue will be used, can be "array" or
35 | # "linked" (default)
36 | task-queue-type = "linked"
37 |
38 | # Allow core threads to time out
39 | allow-core-timeout = on
40 |
41 | }
42 | # Throughput defines the maximum number of messages to be
43 | # processed per actor before the thread jumps to the next actor.
44 | # Set to 1 for as fair as possible.
45 | throughput = 1
46 | }
--------------------------------------------------------------------------------
/springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/EntityActorTest.java:
--------------------------------------------------------------------------------
1 | package com.spring.akka.eventsourcing;
2 |
3 | import java.util.concurrent.CompletableFuture;
4 | import java.util.concurrent.TimeUnit;
5 |
6 | import org.junit.Assert;
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 | import org.springframework.beans.factory.annotation.Autowired;
10 | import org.springframework.boot.test.context.SpringBootTest;
11 | import org.springframework.test.context.junit4.SpringRunner;
12 |
13 | import com.spring.akka.eventsourcing.example.OrderEntity;
14 | import com.spring.akka.eventsourcing.example.OrderEntityProperties;
15 | import com.spring.akka.eventsourcing.example.OrderState;
16 | import com.spring.akka.eventsourcing.example.OrderStatus;
17 | import com.spring.akka.eventsourcing.example.commands.OrderCmd;
18 | import com.spring.akka.eventsourcing.example.response.Response;
19 | import com.spring.akka.eventsourcing.persistance.eventsourcing.PersistentEntityBroker;
20 |
21 | import akka.Done;
22 | import akka.actor.ActorRef;
23 | import akka.pattern.PatternsCS;
24 | import akka.util.Timeout;
25 |
26 | /**
27 | * These test cases are time sensitive and may (extremely rarely) fail if run with test suite because of GC pause etc.
28 | * Run them as part of separate suite or increase the expiry delay to higher number and adjust test cases delays accordingly.
29 | */
30 | @SpringBootTest(classes = TestConfig.class)
31 | @RunWith(SpringRunner.class)
32 | public class EntityActorTest {
33 |
34 | @Autowired
35 | PersistentEntityBroker persistentEntityBroker;
36 |
37 | @Autowired
38 | OrderEntityProperties actorProperties;
39 |
40 | @Test
41 | public void actorShouldGoToDifferentStagesProperly() {
42 |
43 | ActorRef testActorEntity = persistentEntityBroker.findPersistentEntity(OrderEntity.class);
44 |
45 | // check the response with order status is created
46 | final CompletableFuture