diff --git a/akka-docs/src/main/paradox/typed/persistence-style.md b/akka-docs/src/main/paradox/typed/persistence-style.md index afdd8a5546..b5aa8c5459 100644 --- a/akka-docs/src/main/paradox/typed/persistence-style.md +++ b/akka-docs/src/main/paradox/typed/persistence-style.md @@ -16,10 +16,13 @@ of the account; `EmptyAccount`, `OpenedAccount`, and `ClosedAccount`. Scala : @@snip [AccountExampleWithEventHandlersInState.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/AccountExampleWithEventHandlersInState.scala) { #account-entity } -TODO include corresponding example in Java +Java +: @@snip [AccountExampleWithEventHandlersInState.java](/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithEventHandlersInState.java) { #account-entity } -Notice how the `eventHandler` delegates to the `applyEvent` in the `Account` (state), which is implemented -in the concrete `EmptyAccount`, `OpenedAccount`, and `ClosedAccount`. +@scala[Notice how the `eventHandler` delegates to the `applyEvent` in the `Account` (state), which is implemented +in the concrete `EmptyAccount`, `OpenedAccount`, and `ClosedAccount`.] +@java[Notice how the `eventHandler` delegates to methods in the concrete `Account` (state) classes; +`EmptyAccount`, `OpenedAccount`, and `ClosedAccount`.] ## Command handlers in the state @@ -28,10 +31,13 @@ We can take the previous bank account example one step further by handling the c Scala : @@snip [AccountExampleWithCommandHandlersInState.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/AccountExampleWithCommandHandlersInState.scala) { #account-entity } -TODO include corresponding example in Java +Java +: @@snip [AccountExampleWithCommandHandlersInState.java](/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithCommandHandlersInState.java) { #account-entity } -Notice how the command handler is delegating to `applyCommand` in the `Account` (state), which is implemented -in the concrete `EmptyAccount`, `OpenedAccount`, and `ClosedAccount`. +@scala[Notice how the command handler is delegating to `applyCommand` in the `Account` (state), which is implemented +in the concrete `EmptyAccount`, `OpenedAccount`, and `ClosedAccount`.] +@java[Notice how the command handler delegates to methods in the concrete `Account` (state) classes; +`EmptyAccount`, `OpenedAccount`, and `ClosedAccount`.] ## Optional initial state @@ -48,4 +54,21 @@ is then used in command and event handlers at the outer layer before delegating Scala : @@snip [AccountExampleWithOptionState.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/AccountExampleWithOptionState.scala) { #account-entity } -TODO include corresponding example in Java +Java +: @@snip [AccountExampleWithNullState.java](/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithNullState.java) { #account-entity } + +@@@ div { .group-java } +## Mutable state + +The state can be mutable or immutable. When it is immutable the event handler returns a new instance of the state +for each change. + +When using mutable state it's important to not send the full state instance as a message to another actor, +e.g. as a reply to a command. Messages must be immutable to avoid concurrency problems. + +The above examples are using immutable state classes and below is corresponding example with mutable state. + +Java +: @@snip [AccountExampleWithNullState.java](/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithMutableState.java) { #account-entity } + +@@@ diff --git a/akka-docs/src/main/paradox/typed/persistence.md b/akka-docs/src/main/paradox/typed/persistence.md index ad705c5e6d..c890b28985 100644 --- a/akka-docs/src/main/paradox/typed/persistence.md +++ b/akka-docs/src/main/paradox/typed/persistence.md @@ -87,48 +87,51 @@ The same event handler is also used when the entity is started up to recover its It is not recommended to perform side effects in the event handler, as those are also executed during recovery of an persistent actor -## Basic example +### Completing the example + +Let's fill in the details of the example. Command and event: Scala -: @@snip [PersistentActorCompileOnyTest.scala](/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorCompileOnlyTest.scala) { #command } +: @@snip [BasicPersistentBehaviorCompileOnly.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/BasicPersistentBehaviorCompileOnly.scala) { #command } Java -: @@snip [PersistentActorCompileOnyTest.java](/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorCompileOnlyTest.java) { #command } +: @@snip [BasicPersistentBehaviorTest.java](/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/BasicPersistentBehaviorTest.java) { #command } -State is a List containing all the events: +State is a List containing the 5 latest items: Scala -: @@snip [PersistentActorCompileOnyTest.scala](/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorCompileOnlyTest.scala) { #state } +: @@snip [BasicPersistentBehaviorCompileOnly.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/BasicPersistentBehaviorCompileOnly.scala) { #state } Java -: @@snip [PersistentActorCompileOnyTest.java](/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorCompileOnlyTest.java) { #state } +: @@snip [BasicPersistentBehaviorTest.java](/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/BasicPersistentBehaviorTest.java) { #state } -The command handler persists the `Cmd` payload in an `Evt`@java[. In this simple example the command handler is defined using a lambda, for the more complicated example below a `CommandHandlerBuilder` is used]: +The command handler persists the `Add` payload in an `Added` event: Scala -: @@snip [PersistentActorCompileOnyTest.scala](/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorCompileOnlyTest.scala) { #command-handler } +: @@snip [BasicPersistentBehaviorCompileOnly.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/BasicPersistentBehaviorCompileOnly.scala) { #command-handler } Java -: @@snip [PersistentActorCompileOnyTest.java](/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorCompileOnlyTest.java) { #command-handler } +: @@snip [BasicPersistentBehaviorTest.java](/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/BasicPersistentBehaviorTest.java) { #command-handler } -The event handler appends the event to the state. This is called after successfully -persisting the event in the database @java[. As with the command handler the event handler is defined using a lambda, see below for a more complicated example using the `EventHandlerBuilder`]: +The event handler appends the item to the state and keeps 5 items. This is called after successfully +persisting the event in the database: Scala -: @@snip [PersistentActorCompileOnyTest.scala](/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorCompileOnlyTest.scala) { #event-handler } +: @@snip [BasicPersistentBehaviorCompileOnly.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/BasicPersistentBehaviorCompileOnly.scala) { #event-handler } Java -: @@snip [PersistentActorCompileOnyTest.java](/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorCompileOnlyTest.java) { #event-handler } +: @@snip [BasicPersistentBehaviorTest.java](/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/BasicPersistentBehaviorTest.java) { #event-handler } -These are used to create a `EventSourcedBehavior`: +@scala[These are used to create a `EventSourcedBehavior`:] +@java[These are defined in an `EventSourcedBehavior`:] Scala -: @@snip [PersistentActorCompileOnyTest.scala](/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorCompileOnlyTest.scala) { #behavior } +: @@snip [BasicPersistentBehaviorCompileOnly.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/BasicPersistentBehaviorCompileOnly.scala) { #behavior } Java -: @@snip [PersistentActorCompileOnyTest.java](/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorCompileOnlyTest.java) { #behavior } +: @@snip [BasicPersistentBehaviorTest.java](/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/BasicPersistentBehaviorTest.java) { #behavior } ## Cluster Sharding and persistence @@ -150,10 +153,10 @@ If the persistent behavior needs to use the `ActorContext`, for example to spawn wrapping construction with `Behaviors.setup`: Scala -: @@snip [PersistentActorCompileOnyTest.scala](/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorCompileOnlyTest.scala) { #actor-context } +: @@snip [BasicPersistentBehaviorCompileOnly.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/BasicPersistentBehaviorCompileOnly.scala) { #actor-context } Java -: @@snip [PersistentActorCompileOnyTest.java](/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorCompileOnlyTest.java) { #actor-context } +: @@snip [BasicPersistentBehaviorTest.java](/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/BasicPersistentBehaviorTest.java) { #actor-context } @@ -305,30 +308,36 @@ there will be compilation errors if the returned effect isn't a `ReplyEffect`, w created with @scala[`Effect.reply`]@java[`Effects().reply`], @scala[`Effect.noReply`]@java[`Effects().noReply`], @scala[`Effect.thenReply`]@java[`Effects().thenReply`], or @scala[`Effect.thenNoReply`]@java[`Effects().thenNoReply`]. +Scala +: @@snip [AccountExampleWithEventHandlersInState.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/AccountExampleWithEventHandlersInState.scala) { #withEnforcedReplies } + +Java +: @@snip [AccountExampleWithNullState.java](/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithEventHandlersInState.java) { #withEnforcedReplies } + +The commands must implement `ExpectingReply` to include the @scala[`ActorRef[ReplyMessageType]`]@java[`ActorRef`] +in a standardized way. + +Scala +: @@snip [AccountExampleWithEventHandlersInState.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/AccountExampleWithEventHandlersInState.scala) { #reply-command } + +Java +: @@snip [AccountExampleWithNullState.java](/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithEventHandlersInState.java) { #reply-command } + +The `ReplyEffect` is created with @scala[`Effect.reply`]@java[`Effects().reply`], @scala[`Effect.noReply`]@java[`Effects().noReply`], +@scala[`Effect.thenReply`]@java[`Effects().thenReply`], or @scala[`Effect.thenNoReply`]@java[`Effects().thenNoReply`]. + +Scala +: @@snip [AccountExampleWithEventHandlersInState.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/AccountExampleWithEventHandlersInState.scala) { #reply } + +Java +: @@snip [AccountExampleWithNullState.java](/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithEventHandlersInState.java) { #reply } + These effects will send the reply message even when @scala[`EventSourcedBehavior.withEnforcedReplies`]@java[`EventSourcedBehaviorWithEnforcedReplies`] is not used, but then there will be no compilation errors if the reply decision is left out. Note that the `noReply` is a way of making conscious decision that a reply shouldn't be sent for a specific command or the reply will be sent later, perhaps after some asynchronous interaction with other actors or services. -Scala -: @@snip [AccountExampleWithEventHandlersInState.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/AccountExampleWithEventHandlersInState.scala) { #reply-command } - -TODO include corresponding example in Java - -When using the reply effect the commands must implement `ExpectingReply` to include the @scala[`ActorRef[ReplyMessageType]`]@java[`ActorRef`] -in a standardized way. - -Scala -: @@snip [AccountExampleWithEventHandlersInState.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/AccountExampleWithEventHandlersInState.scala) { #reply } - -TODO include corresponding example in Java - -Scala -: @@snip [AccountExampleWithEventHandlersInState.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/AccountExampleWithEventHandlersInState.scala) { #withEnforcedReplies } - -TODO include corresponding example in Java - ## Serialization The same @ref:[serialization](../serialization.md) mechanism as for untyped diff --git a/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorCompileOnlyTest.java b/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorCompileOnlyTest.java index 67ff556fc9..054fa18d83 100644 --- a/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorCompileOnlyTest.java +++ b/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorCompileOnlyTest.java @@ -51,7 +51,6 @@ public class PersistentActorCompileOnlyTest { } // #event-wrapper - // #command public static class SimpleCommand { public final String data; @@ -59,9 +58,7 @@ public class PersistentActorCompileOnlyTest { this.data = data; } } - // #command - // #event static class SimpleEvent { private final String data; @@ -69,9 +66,7 @@ public class PersistentActorCompileOnlyTest { this.data = data; } } - // #event - // #state static class SimpleState { private final List events; @@ -89,9 +84,7 @@ public class PersistentActorCompileOnlyTest { return new SimpleState(newEvents); } } - // #state - // #behavior public static EventSourcedBehavior pb = new EventSourcedBehavior(new PersistenceId("p1")) { @@ -100,19 +93,15 @@ public class PersistentActorCompileOnlyTest { return new SimpleState(); } - // #command-handler @Override public CommandHandler commandHandler() { return (state, cmd) -> Effect().persist(new SimpleEvent(cmd.data)); } - // #command-handler - // #event-handler @Override public EventHandler eventHandler() { return (state, event) -> state.addEvent(event); } - // #event-handler // #install-event-adapter @Override @@ -121,8 +110,6 @@ public class PersistentActorCompileOnlyTest { } // #install-event-adapter }; - - // #behavior } abstract static class WithAck { @@ -287,14 +274,10 @@ public class PersistentActorCompileOnlyTest { what.thenApply(r -> new AcknowledgeSideEffect(r.correlationId)).thenAccept(sender::tell); } - // #actor-context public Behavior behavior(PersistenceId persistenceId) { return Behaviors.setup(ctx -> new MyPersistentBehavior(persistenceId, ctx)); } - // #actor-context - - // #actor-context class MyPersistentBehavior extends EventSourcedBehavior { @@ -305,7 +288,6 @@ public class PersistentActorCompileOnlyTest { super(persistenceId); this.ctx = ctx; } - // #actor-context @Override public EventsInFlight emptyState() { diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExample.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExample.java deleted file mode 100644 index 27a89484ef..0000000000 --- a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExample.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (C) 2018-2019 Lightbend Inc. - */ - -package jdocs.akka.persistence.typed; - -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.persistence.typed.PersistenceId; -import akka.persistence.typed.javadsl.*; - -public class AccountExample - extends EventSourcedBehavior< - AccountExample.AccountCommand, AccountExample.AccountEvent, AccountExample.Account> { - - interface AccountCommand {} - - public static class CreateAccount implements AccountCommand {} - - public static class Deposit implements AccountCommand { - public final double amount; - - public Deposit(double amount) { - this.amount = amount; - } - } - - public static class Withdraw implements AccountCommand { - public final double amount; - - public Withdraw(double amount) { - this.amount = amount; - } - } - - public static class CloseAccount implements AccountCommand {} - - interface AccountEvent {} - - public static class AccountCreated implements AccountEvent {} - - public static class Deposited implements AccountEvent { - public final double amount; - - Deposited(double amount) { - this.amount = amount; - } - } - - public static class Withdrawn implements AccountEvent { - public final double amount; - - Withdrawn(double amount) { - this.amount = amount; - } - } - - public static class AccountClosed implements AccountEvent {} - - interface Account {} - - public static class EmptyAccount implements Account {} - - public static class OpenedAccount implements Account { - public final double balance; - - OpenedAccount(double balance) { - this.balance = balance; - } - } - - public static class ClosedAccount implements Account {} - - public static Behavior behavior(String accountNumber) { - return Behaviors.setup(context -> new AccountExample(context, accountNumber)); - } - - public AccountExample(ActorContext context, String accountNumber) { - super(new PersistenceId(accountNumber)); - } - - @Override - public Account emptyState() { - return new EmptyAccount(); - } - - private CommandHandlerBuilderByState - initialCmdHandler() { - return newCommandHandlerBuilder() - .forStateType(EmptyAccount.class) - .matchCommand(CreateAccount.class, (__, cmd) -> Effect().persist(new AccountCreated())); - } - - private CommandHandlerBuilderByState - openedAccountCmdHandler() { - return newCommandHandlerBuilder() - .forStateType(OpenedAccount.class) - .matchCommand(Deposit.class, (__, cmd) -> Effect().persist(new Deposited(cmd.amount))) - .matchCommand( - Withdraw.class, - (acc, cmd) -> { - if ((acc.balance - cmd.amount) < 0.0) { - return Effect().unhandled(); // TODO replies are missing in this example - } else { - return Effect() - .persist(new Withdrawn(cmd.amount)) - .thenRun( - acc2 -> { // FIXME in scaladsl it's named thenRun, change javadsl also? - // we know this cast is safe, but somewhat ugly - OpenedAccount openAccount = (OpenedAccount) acc2; - // do some side-effect using balance - System.out.println(openAccount.balance); - }); - } - }) - .matchCommand( - CloseAccount.class, - (acc, cmd) -> { - if (acc.balance == 0.0) return Effect().persist(new AccountClosed()); - else return Effect().unhandled(); - }); - } - - private CommandHandlerBuilderByState - closedCmdHandler() { - return newCommandHandlerBuilder() - .forStateType(ClosedAccount.class) - .matchCommand(AccountCommand.class, __ -> Effect().unhandled()); - } - - @Override - public CommandHandler commandHandler() { - return initialCmdHandler().orElse(openedAccountCmdHandler()).orElse(closedCmdHandler()).build(); - } - - private EventHandlerBuilderByState initialEvtHandler() { - return newEventHandlerBuilder() - .forStateType(EmptyAccount.class) - .matchEvent(AccountCreated.class, () -> new OpenedAccount(0.0)); - } - - private EventHandlerBuilderByState - openedAccountEvtHandler() { - return newEventHandlerBuilder() - .forStateType(OpenedAccount.class) - .matchEvent(Deposited.class, (acc, cmd) -> new OpenedAccount(acc.balance + cmd.amount)) - .matchEvent(Withdrawn.class, (acc, cmd) -> new OpenedAccount(acc.balance - cmd.amount)) - .matchEvent(AccountClosed.class, ClosedAccount::new); - } - - @Override - public EventHandler eventHandler() { - return initialEvtHandler().orElse(openedAccountEvtHandler()).build(); - } -} diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLiners.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLiners.java deleted file mode 100644 index 62b6131947..0000000000 --- a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLiners.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2018-2019 Lightbend Inc. - */ - -package jdocs.akka.persistence.typed; - -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.persistence.typed.PersistenceId; -import akka.persistence.typed.javadsl.*; - -public class AccountExampleOneLiners - extends EventSourcedBehavior< - AccountExampleOneLiners.AccountCommand, - AccountExampleOneLiners.AccountEvent, - AccountExampleOneLiners.Account> { - - interface AccountCommand {} - - public static class CreateAccount implements AccountCommand {} - - public static class Deposit implements AccountCommand { - public final double amount; - - public Deposit(double amount) { - this.amount = amount; - } - } - - public static class Withdraw implements AccountCommand { - public final double amount; - - public Withdraw(double amount) { - this.amount = amount; - } - } - - public static class CloseAccount implements AccountCommand {} - - interface AccountEvent {} - - public static class AccountCreated implements AccountEvent {} - - public static class Deposited implements AccountEvent { - public final double amount; - - Deposited(double amount) { - this.amount = amount; - } - } - - public static class Withdrawn implements AccountEvent { - public final double amount; - - Withdrawn(double amount) { - this.amount = amount; - } - } - - public static class AccountClosed implements AccountEvent {} - - interface Account {} - - public static class EmptyAccount implements Account {} - - public static class OpenedAccount implements Account { - public final double balance; - - OpenedAccount(double balance) { - this.balance = balance; - } - } - - public static class ClosedAccount implements Account {} - - public static Behavior behavior(String accountNumber) { - return Behaviors.setup(context -> new AccountExampleOneLiners(context, accountNumber)); - } - - public AccountExampleOneLiners(ActorContext context, String accountNumber) { - super(new PersistenceId(accountNumber)); - } - - @Override - public Account emptyState() { - return new EmptyAccount(); - } - - private Effect createAccount() { - return Effect().persist(new AccountCreated()); - } - - private Effect depositCommand(Deposit deposit) { - return Effect().persist(new Deposited(deposit.amount)); - } - - private Effect withdrawCommand(OpenedAccount account, Withdraw withdraw) { - if ((account.balance - withdraw.amount) < 0.0) { - return Effect().unhandled(); // TODO replies are missing in this example - } else { - return Effect() - .persist(new Withdrawn(withdraw.amount)) - .thenRun( - acc2 -> { - // we know this cast is safe, but somewhat ugly - OpenedAccount openAccount = (OpenedAccount) acc2; - // do some side-effect using balance - System.out.println(openAccount.balance); - }); - } - } - - private Effect closeCommand(OpenedAccount account, CloseAccount cmd) { - if (account.balance == 0.0) return Effect().persist(new AccountClosed()); - else return Effect().unhandled(); - } - - private CommandHandlerBuilderByState - initialCmdHandler() { - return newCommandHandlerBuilder() - .forStateType(EmptyAccount.class) - .matchCommand(CreateAccount.class, this::createAccount); - } - - private CommandHandlerBuilderByState - openedAccountCmdHandler() { - return newCommandHandlerBuilder() - .forStateType(OpenedAccount.class) - .matchCommand(Deposit.class, this::depositCommand) - .matchCommand(Withdraw.class, this::withdrawCommand) - .matchCommand(CloseAccount.class, this::closeCommand); - } - - private CommandHandlerBuilderByState - closedCmdHandler() { - return newCommandHandlerBuilder() - .forStateType(ClosedAccount.class) - .matchCommand(AccountCommand.class, __ -> Effect().unhandled()); - } - - @Override - public CommandHandler commandHandler() { - return initialCmdHandler().orElse(openedAccountCmdHandler()).orElse(closedCmdHandler()).build(); - } - - private OpenedAccount openAccount() { - return new OpenedAccount(0.0); - } - - private OpenedAccount makeDeposit(OpenedAccount acc, Deposited deposit) { - return new OpenedAccount(acc.balance + deposit.amount); - } - - private OpenedAccount makeWithdraw(OpenedAccount acc, Withdrawn withdrawn) { - return new OpenedAccount(acc.balance - withdrawn.amount); - } - - private ClosedAccount closeAccount() { - return new ClosedAccount(); - } - - private EventHandlerBuilderByState initialEvtHandler() { - return newEventHandlerBuilder() - .forStateType(EmptyAccount.class) - .matchEvent(AccountCreated.class, this::openAccount); - } - - private EventHandlerBuilderByState - openedAccountEvtHandler() { - return newEventHandlerBuilder() - .forStateType(OpenedAccount.class) - .matchEvent(Deposited.class, this::makeDeposit) - .matchEvent(Withdrawn.class, this::makeWithdraw) - .matchEvent(AccountClosed.class, ClosedAccount::new); - } - - @Override - public EventHandler eventHandler() { - return initialEvtHandler().orElse(openedAccountEvtHandler()).build(); - } -} diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersInModel.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersInModel.java deleted file mode 100644 index d0b8f0da72..0000000000 --- a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersInModel.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (C) 2018-2019 Lightbend Inc. - */ - -package jdocs.akka.persistence.typed; - -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.persistence.typed.PersistenceId; -import akka.persistence.typed.javadsl.*; - -public class AccountExampleOneLinersInModel - extends EventSourcedBehavior< - AccountExampleOneLinersInModel.AccountCommand, - AccountExampleOneLinersInModel.AccountEvent, - AccountExampleOneLinersInModel.Account> { - - interface AccountCommand {} - - public static class CreateAccount implements AccountCommand {} - - public static class Deposit implements AccountCommand { - public final double amount; - - public Deposit(double amount) { - this.amount = amount; - } - } - - public static class Withdraw implements AccountCommand { - public final double amount; - - public Withdraw(double amount) { - this.amount = amount; - } - } - - public static class CloseAccount implements AccountCommand {} - - interface AccountEvent {} - - public static class AccountCreated implements AccountEvent {} - - public static class Deposited implements AccountEvent { - public final double amount; - - Deposited(double amount) { - this.amount = amount; - } - } - - public static class Withdrawn implements AccountEvent { - public final double amount; - - Withdrawn(double amount) { - this.amount = amount; - } - } - - public static class AccountClosed implements AccountEvent {} - - interface Account {} - - public class EmptyAccount implements Account { - - Effect createAccount(CreateAccount cmd) { - return Effect().persist(new AccountCreated()); - } - - OpenedAccount openAccount(AccountCreated evt) { - return new OpenedAccount(0.0); - } - } - - public class OpenedAccount implements Account { - public final double balance; - - OpenedAccount(double balance) { - this.balance = balance; - } - - Effect depositCommand(Deposit deposit) { - return Effect().persist(new Deposited(deposit.amount)); - } - - Effect withdrawCommand(Withdraw withdraw) { - if ((balance - withdraw.amount) < 0.0) { - return Effect().unhandled(); // TODO replies are missing in this example - } else { - return Effect() - .persist(new Withdrawn(withdraw.amount)) - .thenRun( - acc2 -> { - // we know this cast is safe, but somewhat ugly - OpenedAccount openAccount = (OpenedAccount) acc2; - // do some side-effect using balance - System.out.println(openAccount.balance); - }); - } - } - - Effect closeCommand(CloseAccount cmd) { - if (balance == 0.0) return Effect().persist(new AccountClosed()); - else return Effect().unhandled(); - } - - OpenedAccount makeDeposit(Deposited deposit) { - return new OpenedAccount(balance + deposit.amount); - } - - OpenedAccount makeWithdraw(Withdrawn withdrawn) { - return new OpenedAccount(balance - withdrawn.amount); - } - - ClosedAccount closeAccount(AccountClosed evt) { - return new ClosedAccount(); - } - } - - public class ClosedAccount implements Account {} - - public static Behavior behavior(String accountNumber) { - return Behaviors.setup(context -> new AccountExampleOneLinersInModel(context, accountNumber)); - } - - public AccountExampleOneLinersInModel( - ActorContext context, String accountNumber) { - super(new PersistenceId(accountNumber)); - } - - @Override - public Account emptyState() { - return new EmptyAccount(); - } - - @Override - public CommandHandler commandHandler() { - CommandHandlerBuilder builder = - newCommandHandlerBuilder(); - - builder - .forStateType(EmptyAccount.class) - .matchCommand(CreateAccount.class, EmptyAccount::createAccount); - - builder - .forStateType(OpenedAccount.class) - .matchCommand(Deposit.class, OpenedAccount::depositCommand) - .matchCommand(Withdraw.class, OpenedAccount::withdrawCommand) - .matchCommand(CloseAccount.class, OpenedAccount::closeCommand); - - builder.forStateType(ClosedAccount.class).matchAny(() -> Effect().unhandled()); - - return builder.build(); - } - - @Override - public EventHandler eventHandler() { - EventHandlerBuilder builder = newEventHandlerBuilder(); - - builder - .forStateType(EmptyAccount.class) - .matchEvent(AccountCreated.class, EmptyAccount::openAccount); - - builder - .forStateType(OpenedAccount.class) - .matchEvent(Deposited.class, OpenedAccount::makeDeposit) - .matchEvent(Withdrawn.class, OpenedAccount::makeWithdraw) - .matchEvent(AccountClosed.class, OpenedAccount::closeAccount); - - return builder.build(); - } -} diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersInModelWithNull.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersInModelWithNull.java deleted file mode 100644 index a948d6fcb4..0000000000 --- a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersInModelWithNull.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (C) 2018-2019 Lightbend Inc. - */ - -package jdocs.akka.persistence.typed; - -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.persistence.typed.PersistenceId; -import akka.persistence.typed.javadsl.*; - -public class AccountExampleOneLinersInModelWithNull - extends EventSourcedBehavior< - AccountExampleOneLinersInModelWithNull.AccountCommand, - AccountExampleOneLinersInModelWithNull.AccountEvent, - AccountExampleOneLinersInModelWithNull.Account> { - - interface AccountCommand {} - - public static class CreateAccount implements AccountCommand {} - - public static class Deposit implements AccountCommand { - public final double amount; - - public Deposit(double amount) { - this.amount = amount; - } - } - - public static class Withdraw implements AccountCommand { - public final double amount; - - public Withdraw(double amount) { - this.amount = amount; - } - } - - public static class CloseAccount implements AccountCommand {} - - interface AccountEvent {} - - public static class AccountCreated implements AccountEvent {} - - public static class Deposited implements AccountEvent { - public final double amount; - - Deposited(double amount) { - this.amount = amount; - } - } - - public static class Withdrawn implements AccountEvent { - public final double amount; - - Withdrawn(double amount) { - this.amount = amount; - } - } - - public static class AccountClosed implements AccountEvent {} - - interface Account {} - - public class OpenedAccount implements Account { - public final double balance; - - OpenedAccount(double balance) { - this.balance = balance; - } - - Effect depositCommand(Deposit deposit) { - return Effect().persist(new Deposited(deposit.amount)); - } - - Effect withdrawCommand(Withdraw withdraw) { - if ((balance - withdraw.amount) < 0.0) { - return Effect().unhandled(); // TODO replies are missing in this example - } else { - return Effect() - .persist(new Withdrawn(withdraw.amount)) - .thenRun( - acc2 -> { - // we know this cast is safe, but somewhat ugly - OpenedAccount openAccount = (OpenedAccount) acc2; - // do some side-effect using balance - System.out.println(openAccount.balance); - }); - } - } - - Effect closeCommand(CloseAccount cmd) { - if (balance == 0.0) return Effect().persist(new AccountClosed()); - else return Effect().unhandled(); - } - - OpenedAccount makeDeposit(Deposited deposit) { - return new OpenedAccount(balance + deposit.amount); - } - - OpenedAccount makeWithdraw(Withdrawn withdrawn) { - return new OpenedAccount(balance - withdrawn.amount); - } - - ClosedAccount closeAccount(AccountClosed cmd) { - return new ClosedAccount(); - } - } - - public class ClosedAccount implements Account {} - - public static Behavior behavior(String accountNumber) { - return Behaviors.setup( - context -> new AccountExampleOneLinersInModelWithNull(context, accountNumber)); - } - - public AccountExampleOneLinersInModelWithNull( - ActorContext context, String accountNumber) { - super(new PersistenceId(accountNumber)); - } - - @Override - public Account emptyState() { - return null; - } - - private Effect createAccount(CreateAccount cmd) { - return Effect().persist(new AccountCreated()); - } - - private OpenedAccount openAccount() { - return new OpenedAccount(0.0); - } - - @Override - public CommandHandler commandHandler() { - - CommandHandlerBuilder builder = - newCommandHandlerBuilder(); - - builder.forNullState().matchCommand(CreateAccount.class, this::createAccount); - - builder - .forStateType(OpenedAccount.class) - .matchCommand(Deposit.class, OpenedAccount::depositCommand) - .matchCommand(Withdraw.class, OpenedAccount::withdrawCommand) - .matchCommand(CloseAccount.class, OpenedAccount::closeCommand); - - builder.forStateType(ClosedAccount.class).matchAny(() -> Effect().unhandled()); - - return builder.build(); - } - - @Override - public EventHandler eventHandler() { - - EventHandlerBuilder builder = newEventHandlerBuilder(); - - builder.forNullState().matchEvent(AccountCreated.class, this::openAccount); - - builder - .forStateType(OpenedAccount.class) - .matchEvent(Deposited.class, OpenedAccount::makeDeposit) - .matchEvent(Withdrawn.class, OpenedAccount::makeWithdraw) - .matchEvent(AccountClosed.class, OpenedAccount::closeAccount); - - return builder.build(); - } -} diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersWithNull.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersWithNull.java deleted file mode 100644 index d9ccee5070..0000000000 --- a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersWithNull.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (C) 2018-2019 Lightbend Inc. - */ - -package jdocs.akka.persistence.typed; - -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.persistence.typed.PersistenceId; -import akka.persistence.typed.javadsl.*; - -public class AccountExampleOneLinersWithNull - extends EventSourcedBehavior< - AccountExampleOneLinersWithNull.AccountCommand, - AccountExampleOneLinersWithNull.AccountEvent, - AccountExampleOneLinersWithNull.Account> { - - interface AccountCommand {} - - public static class CreateAccount implements AccountCommand {} - - public static class Deposit implements AccountCommand { - public final double amount; - - public Deposit(double amount) { - this.amount = amount; - } - } - - public static class Withdraw implements AccountCommand { - public final double amount; - - public Withdraw(double amount) { - this.amount = amount; - } - } - - public static class CloseAccount implements AccountCommand {} - - interface AccountEvent {} - - public static class AccountCreated implements AccountEvent {} - - public static class Deposited implements AccountEvent { - public final double amount; - - Deposited(double amount) { - this.amount = amount; - } - } - - public static class Withdrawn implements AccountEvent { - public final double amount; - - Withdrawn(double amount) { - this.amount = amount; - } - } - - public static class AccountClosed implements AccountEvent {} - - interface Account {} - - public static class OpenedAccount implements Account { - public final double balance; - - OpenedAccount(double balance) { - this.balance = balance; - } - } - - public static class ClosedAccount implements Account {} - - public static Behavior behavior(String accountNumber) { - return Behaviors.setup(context -> new AccountExampleOneLinersWithNull(context, accountNumber)); - } - - public AccountExampleOneLinersWithNull( - ActorContext context, String accountNumber) { - super(new PersistenceId(accountNumber)); - } - - @Override - public Account emptyState() { - return null; - } - - private Effect createAccount() { - return Effect().persist(new AccountCreated()); - } - - private Effect depositCommand(Deposit deposit) { - return Effect().persist(new Deposited(deposit.amount)); - } - - private Effect withdrawCommand(OpenedAccount account, Withdraw withdraw) { - if ((account.balance - withdraw.amount) < 0.0) { - return Effect().unhandled(); // TODO replies are missing in this example - } else { - return Effect() - .persist(new Withdrawn(withdraw.amount)) - .thenRun( - acc2 -> { - // we know this cast is safe, but somewhat ugly - OpenedAccount openAccount = (OpenedAccount) acc2; - // do some side-effect using balance - System.out.println(openAccount.balance); - }); - } - } - - private Effect closeCommand(OpenedAccount account, CloseAccount cmd) { - if (account.balance == 0.0) return Effect().persist(new AccountClosed()); - else return Effect().unhandled(); - } - - private CommandHandlerBuilderByState - initialHandler() { - return newCommandHandlerBuilder() - .forNullState() - .matchCommand(CreateAccount.class, this::createAccount); - } - - private CommandHandlerBuilderByState - openedAccountHandler() { - return newCommandHandlerBuilder() - .forStateType(OpenedAccount.class) - .matchCommand(Deposit.class, this::depositCommand) - .matchCommand(Withdraw.class, this::withdrawCommand) - .matchCommand(CloseAccount.class, this::closeCommand); - } - - private CommandHandlerBuilderByState - closedHandler() { - return newCommandHandlerBuilder() - .forStateType(ClosedAccount.class) - .matchCommand(AccountCommand.class, __ -> Effect().unhandled()); - } - - @Override - public CommandHandler commandHandler() { - return initialHandler().orElse(openedAccountHandler()).orElse(closedHandler()).build(); - } - - private OpenedAccount openAccount() { - return new OpenedAccount(0.0); - } - - private OpenedAccount makeDeposit(OpenedAccount acc, Deposited deposit) { - return new OpenedAccount(acc.balance + deposit.amount); - } - - private OpenedAccount makeWithdraw(OpenedAccount acc, Withdrawn withdrawn) { - return new OpenedAccount(acc.balance - withdrawn.amount); - } - - private ClosedAccount closeAccount() { - return new ClosedAccount(); - } - - private EventHandlerBuilderByState initialEvtHandler() { - return newEventHandlerBuilder() - .forNullState() - .matchEvent(AccountCreated.class, this::openAccount); - } - - private EventHandlerBuilderByState - openedAccountEvtHandler() { - return newEventHandlerBuilder() - .forStateType(OpenedAccount.class) - .matchEvent(Deposited.class, this::makeDeposit) - .matchEvent(Withdrawn.class, this::makeWithdraw) - .matchEvent(AccountClosed.class, ClosedAccount::new); - } - - @Override - public EventHandler eventHandler() { - return initialEvtHandler().orElse(openedAccountEvtHandler()).build(); - } -} diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithCommandHandlersInState.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithCommandHandlersInState.java new file mode 100644 index 0000000000..f5a827530f --- /dev/null +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithCommandHandlersInState.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2018-2019 Lightbend Inc. + */ + +package jdocs.akka.persistence.typed; + +import akka.actor.typed.ActorRef; +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.ActorContext; +import akka.actor.typed.javadsl.Behaviors; +import akka.persistence.typed.ExpectingReply; +import akka.persistence.typed.PersistenceId; +import akka.persistence.typed.javadsl.CommandHandler; +import akka.persistence.typed.javadsl.CommandHandlerBuilder; +import akka.persistence.typed.javadsl.EventHandler; +import akka.persistence.typed.javadsl.EventHandlerBuilder; +import akka.persistence.typed.javadsl.EventSourcedBehaviorWithEnforcedReplies; +import akka.persistence.typed.javadsl.ReplyEffect; + +import java.math.BigDecimal; + +/** + * Bank account example illustrating: - different state classes representing the lifecycle of the + * account - event handlers that delegate to methods in the state classes - command handlers that + * delegate to methods in the state classes - replies of various types, using ExpectingReply and + * EventSourcedBehaviorWithEnforcedReplies + */ +public interface AccountExampleWithCommandHandlersInState { + + // #account-entity + public class AccountEntity + extends EventSourcedBehaviorWithEnforcedReplies< + AccountEntity.AccountCommand, AccountEntity.AccountEvent, AccountEntity.Account> { + + // Command + interface AccountCommand extends ExpectingReply {} + + public static class CreateAccount implements AccountCommand { + private final ActorRef replyTo; + + public CreateAccount(ActorRef replyTo) { + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class Deposit implements AccountCommand { + public final BigDecimal amount; + private final ActorRef replyTo; + + public Deposit(BigDecimal amount, ActorRef replyTo) { + this.replyTo = replyTo; + this.amount = amount; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class Withdraw implements AccountCommand { + public final BigDecimal amount; + private final ActorRef replyTo; + + public Withdraw(BigDecimal amount, ActorRef replyTo) { + this.amount = amount; + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class GetBalance implements AccountCommand { + private final ActorRef replyTo; + + public GetBalance(ActorRef replyTo) { + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class CloseAccount implements AccountCommand { + private final ActorRef replyTo; + + public CloseAccount(ActorRef replyTo) { + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + // Reply + interface AccountCommandReply {} + + interface OperationResult extends AccountCommandReply {} + + enum Confirmed implements OperationResult { + INSTANCE + } + + public static class Rejected implements OperationResult { + public final String reason; + + public Rejected(String reason) { + this.reason = reason; + } + } + + public static class CurrentBalance implements AccountCommandReply { + public final BigDecimal balance; + + public CurrentBalance(BigDecimal balance) { + this.balance = balance; + } + } + + // Event + interface AccountEvent {} + + public static class AccountCreated implements AccountEvent {} + + public static class Deposited implements AccountEvent { + public final BigDecimal amount; + + Deposited(BigDecimal amount) { + this.amount = amount; + } + } + + public static class Withdrawn implements AccountEvent { + public final BigDecimal amount; + + Withdrawn(BigDecimal amount) { + this.amount = amount; + } + } + + public static class AccountClosed implements AccountEvent {} + + // State + interface Account {} + + public class EmptyAccount implements Account { + ReplyEffect createAccount(CreateAccount command) { + return Effect() + .persist(new AccountCreated()) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } + + OpenedAccount openedAccount() { + return new OpenedAccount(BigDecimal.ZERO); + } + } + + public class OpenedAccount implements Account { + public final BigDecimal balance; + + public OpenedAccount(BigDecimal balance) { + this.balance = balance; + } + + ReplyEffect deposit(Deposit command) { + return Effect() + .persist(new Deposited(command.amount)) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } + + ReplyEffect withdraw(Withdraw command) { + if (!canWithdraw(command.amount)) { + return Effect() + .reply(command, new Rejected("not enough funds to withdraw " + command.amount)); + } else { + return Effect() + .persist(new Withdrawn(command.amount)) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } + } + + ReplyEffect getBalance(GetBalance command) { + return Effect().reply(command, new CurrentBalance(balance)); + } + + ReplyEffect closeAccount(CloseAccount command) { + if (balance.equals(BigDecimal.ZERO)) { + return Effect() + .persist(new AccountClosed()) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } else { + return Effect().reply(command, new Rejected("balance must be zero for closing account")); + } + } + + OpenedAccount makeDeposit(BigDecimal amount) { + return new OpenedAccount(balance.add(amount)); + } + + boolean canWithdraw(BigDecimal amount) { + return (balance.subtract(amount).compareTo(BigDecimal.ZERO) >= 0); + } + + OpenedAccount makeWithdraw(BigDecimal amount) { + if (!canWithdraw(amount)) + throw new IllegalStateException("Account balance can't be negative"); + return new OpenedAccount(balance.subtract(amount)); + } + + ClosedAccount closedAccount() { + return new ClosedAccount(); + } + } + + public static class ClosedAccount implements Account {} + + public static Behavior behavior(String accountNumber) { + return Behaviors.setup(context -> new AccountEntity(context, accountNumber)); + } + + public AccountEntity(ActorContext context, String accountNumber) { + super(new PersistenceId(accountNumber)); + } + + @Override + public Account emptyState() { + return new EmptyAccount(); + } + + @Override + public CommandHandler commandHandler() { + CommandHandlerBuilder builder = + newCommandHandlerBuilder(); + + builder + .forStateType(EmptyAccount.class) + .matchCommand(CreateAccount.class, EmptyAccount::createAccount); + + builder + .forStateType(OpenedAccount.class) + .matchCommand(Deposit.class, OpenedAccount::deposit) + .matchCommand(Withdraw.class, OpenedAccount::withdraw) + .matchCommand(GetBalance.class, OpenedAccount::getBalance) + .matchCommand(CloseAccount.class, OpenedAccount::closeAccount); + + builder.forStateType(ClosedAccount.class).matchAny(() -> Effect().unhandled()); + + return builder.build(); + } + + @Override + public EventHandler eventHandler() { + EventHandlerBuilder builder = newEventHandlerBuilder(); + + builder + .forStateType(EmptyAccount.class) + .matchEvent(AccountCreated.class, (account, event) -> account.openedAccount()); + + builder + .forStateType(OpenedAccount.class) + .matchEvent( + Deposited.class, + (account, deposited) -> { + account.makeDeposit(deposited.amount); + return account; + }) + .matchEvent( + Withdrawn.class, + (account, withdrawn) -> { + account.makeWithdraw(withdrawn.amount); + return account; + }) + .matchEvent(AccountClosed.class, (account, closed) -> account.closedAccount()); + + return builder.build(); + } + } + + // #account-entity +} diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithEventHandlersInState.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithEventHandlersInState.java new file mode 100644 index 0000000000..67eff7c96a --- /dev/null +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithEventHandlersInState.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2018-2019 Lightbend Inc. + */ + +package jdocs.akka.persistence.typed; + +import akka.actor.typed.ActorRef; +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.ActorContext; +import akka.actor.typed.javadsl.Behaviors; +import akka.persistence.typed.ExpectingReply; +import akka.persistence.typed.PersistenceId; +import akka.persistence.typed.javadsl.CommandHandler; +import akka.persistence.typed.javadsl.CommandHandlerBuilder; +import akka.persistence.typed.javadsl.ReplyEffect; +import akka.persistence.typed.javadsl.EventHandler; +import akka.persistence.typed.javadsl.EventHandlerBuilder; +import akka.persistence.typed.javadsl.EventSourcedBehaviorWithEnforcedReplies; + +import java.math.BigDecimal; + +/** + * Bank account example illustrating: - different state classes representing the lifecycle of the + * account - event handlers that delegate to methods in the state classes - command handlers that + * delegate to methods in the EventSourcedBehavior class - replies of various types, using + * ExpectingReply and EventSourcedBehaviorWithEnforcedReplies + */ +public interface AccountExampleWithEventHandlersInState { + + // #account-entity + // #withEnforcedReplies + public class AccountEntity + extends EventSourcedBehaviorWithEnforcedReplies< + AccountEntity.AccountCommand, AccountEntity.AccountEvent, AccountEntity.Account> { + // #withEnforcedReplies + + // Command + // #reply-command + interface AccountCommand extends ExpectingReply {} + // #reply-command + + public static class CreateAccount implements AccountCommand { + private final ActorRef replyTo; + + public CreateAccount(ActorRef replyTo) { + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class Deposit implements AccountCommand { + public final BigDecimal amount; + private final ActorRef replyTo; + + public Deposit(BigDecimal amount, ActorRef replyTo) { + this.replyTo = replyTo; + this.amount = amount; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class Withdraw implements AccountCommand { + public final BigDecimal amount; + private final ActorRef replyTo; + + public Withdraw(BigDecimal amount, ActorRef replyTo) { + this.amount = amount; + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class GetBalance implements AccountCommand { + private final ActorRef replyTo; + + public GetBalance(ActorRef replyTo) { + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class CloseAccount implements AccountCommand { + private final ActorRef replyTo; + + public CloseAccount(ActorRef replyTo) { + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + // Reply + // #reply-command + interface AccountCommandReply {} + + interface OperationResult extends AccountCommandReply {} + + enum Confirmed implements OperationResult { + INSTANCE + } + + public static class Rejected implements OperationResult { + public final String reason; + + public Rejected(String reason) { + this.reason = reason; + } + } + // #reply-command + + public static class CurrentBalance implements AccountCommandReply { + public final BigDecimal balance; + + public CurrentBalance(BigDecimal balance) { + this.balance = balance; + } + } + + // Event + interface AccountEvent {} + + public static class AccountCreated implements AccountEvent {} + + public static class Deposited implements AccountEvent { + public final BigDecimal amount; + + Deposited(BigDecimal amount) { + this.amount = amount; + } + } + + public static class Withdrawn implements AccountEvent { + public final BigDecimal amount; + + Withdrawn(BigDecimal amount) { + this.amount = amount; + } + } + + public static class AccountClosed implements AccountEvent {} + + // State + interface Account {} + + public static class EmptyAccount implements Account { + OpenedAccount openedAccount() { + return new OpenedAccount(BigDecimal.ZERO); + } + } + + public static class OpenedAccount implements Account { + private final BigDecimal balance; + + public OpenedAccount(BigDecimal balance) { + this.balance = balance; + } + + OpenedAccount makeDeposit(BigDecimal amount) { + return new OpenedAccount(balance.add(amount)); + } + + boolean canWithdraw(BigDecimal amount) { + return (balance.subtract(amount).compareTo(BigDecimal.ZERO) >= 0); + } + + OpenedAccount makeWithdraw(BigDecimal amount) { + if (!canWithdraw(amount)) + throw new IllegalStateException("Account balance can't be negative"); + return new OpenedAccount(balance.subtract(amount)); + } + + ClosedAccount closedAccount() { + return new ClosedAccount(); + } + } + + public static class ClosedAccount implements Account {} + + public static Behavior behavior(String accountNumber) { + return Behaviors.setup(context -> new AccountEntity(context, accountNumber)); + } + + public AccountEntity(ActorContext context, String accountNumber) { + super(new PersistenceId(accountNumber)); + } + + @Override + public Account emptyState() { + return new EmptyAccount(); + } + + @Override + public CommandHandler commandHandler() { + CommandHandlerBuilder builder = + newCommandHandlerBuilder(); + + builder + .forStateType(EmptyAccount.class) + .matchCommand(CreateAccount.class, this::createAccount); + + builder + .forStateType(OpenedAccount.class) + .matchCommand(Deposit.class, this::deposit) + .matchCommand(Withdraw.class, this::withdraw) + .matchCommand(GetBalance.class, this::getBalance) + .matchCommand(CloseAccount.class, this::closeAccount); + + builder.forStateType(ClosedAccount.class).matchAny(() -> Effect().unhandled()); + + return builder.build(); + } + + private ReplyEffect createAccount( + EmptyAccount account, CreateAccount command) { + return Effect() + .persist(new AccountCreated()) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } + + private ReplyEffect deposit(OpenedAccount account, Deposit command) { + return Effect() + .persist(new Deposited(command.amount)) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } + + // #reply + private ReplyEffect withdraw(OpenedAccount account, Withdraw command) { + if (!account.canWithdraw(command.amount)) { + return Effect() + .reply(command, new Rejected("not enough funds to withdraw " + command.amount)); + } else { + return Effect() + .persist(new Withdrawn(command.amount)) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } + } + // #reply + + private ReplyEffect getBalance( + OpenedAccount account, GetBalance command) { + return Effect().reply(command, new CurrentBalance(account.balance)); + } + + private ReplyEffect closeAccount( + OpenedAccount account, CloseAccount command) { + if (account.balance.equals(BigDecimal.ZERO)) { + return Effect() + .persist(new AccountClosed()) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } else { + return Effect().reply(command, new Rejected("balance must be zero for closing account")); + } + } + + @Override + public EventHandler eventHandler() { + EventHandlerBuilder builder = newEventHandlerBuilder(); + + builder + .forStateType(EmptyAccount.class) + .matchEvent(AccountCreated.class, (account, created) -> account.openedAccount()); + + builder + .forStateType(OpenedAccount.class) + .matchEvent( + Deposited.class, (account, deposited) -> account.makeDeposit(deposited.amount)) + .matchEvent( + Withdrawn.class, (account, withdrawn) -> account.makeWithdraw(withdrawn.amount)) + .matchEvent(AccountClosed.class, (account, closed) -> account.closedAccount()); + + return builder.build(); + } + } + + // #account-entity +} diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithMutableState.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithMutableState.java new file mode 100644 index 0000000000..4907dcb25d --- /dev/null +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithMutableState.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2018-2019 Lightbend Inc. + */ + +package jdocs.akka.persistence.typed; + +import akka.actor.typed.ActorRef; +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.ActorContext; +import akka.actor.typed.javadsl.Behaviors; +import akka.persistence.typed.ExpectingReply; +import akka.persistence.typed.PersistenceId; +import akka.persistence.typed.javadsl.CommandHandler; +import akka.persistence.typed.javadsl.CommandHandlerBuilder; +import akka.persistence.typed.javadsl.EventHandler; +import akka.persistence.typed.javadsl.EventHandlerBuilder; +import akka.persistence.typed.javadsl.EventSourcedBehaviorWithEnforcedReplies; +import akka.persistence.typed.javadsl.ReplyEffect; + +import java.math.BigDecimal; + +/** + * Bank account example illustrating: - different state classes representing the lifecycle of the + * account - mutable state - event handlers that delegate to methods in the state classes - command + * handlers that delegate to methods in the EventSourcedBehavior class - replies of various types, + * using ExpectingReply and EventSourcedBehaviorWithEnforcedReplies + */ +public interface AccountExampleWithMutableState { + + // #account-entity + public class AccountEntity + extends EventSourcedBehaviorWithEnforcedReplies< + AccountEntity.AccountCommand, AccountEntity.AccountEvent, AccountEntity.Account> { + + // Command + interface AccountCommand extends ExpectingReply {} + + public static class CreateAccount implements AccountCommand { + private final ActorRef replyTo; + + public CreateAccount(ActorRef replyTo) { + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class Deposit implements AccountCommand { + public final BigDecimal amount; + private final ActorRef replyTo; + + public Deposit(BigDecimal amount, ActorRef replyTo) { + this.replyTo = replyTo; + this.amount = amount; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class Withdraw implements AccountCommand { + public final BigDecimal amount; + private final ActorRef replyTo; + + public Withdraw(BigDecimal amount, ActorRef replyTo) { + this.amount = amount; + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class GetBalance implements AccountCommand { + private final ActorRef replyTo; + + public GetBalance(ActorRef replyTo) { + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class CloseAccount implements AccountCommand { + private final ActorRef replyTo; + + public CloseAccount(ActorRef replyTo) { + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + // Reply + interface AccountCommandReply {} + + interface OperationResult extends AccountCommandReply {} + + enum Confirmed implements OperationResult { + INSTANCE + } + + public static class Rejected implements OperationResult { + public final String reason; + + public Rejected(String reason) { + this.reason = reason; + } + } + + public static class CurrentBalance implements AccountCommandReply { + public final BigDecimal balance; + + public CurrentBalance(BigDecimal balance) { + this.balance = balance; + } + } + + // Event + interface AccountEvent {} + + public static class AccountCreated implements AccountEvent {} + + public static class Deposited implements AccountEvent { + public final BigDecimal amount; + + Deposited(BigDecimal amount) { + this.amount = amount; + } + } + + public static class Withdrawn implements AccountEvent { + public final BigDecimal amount; + + Withdrawn(BigDecimal amount) { + this.amount = amount; + } + } + + public static class AccountClosed implements AccountEvent {} + + // State + interface Account {} + + public static class EmptyAccount implements Account { + OpenedAccount openedAccount() { + return new OpenedAccount(); + } + } + + public static class OpenedAccount implements Account { + private BigDecimal balance = BigDecimal.ZERO; + + public BigDecimal getBalance() { + return balance; + } + + void makeDeposit(BigDecimal amount) { + balance = balance.add(amount); + } + + boolean canWithdraw(BigDecimal amount) { + return (balance.subtract(amount).compareTo(BigDecimal.ZERO) >= 0); + } + + void makeWithdraw(BigDecimal amount) { + if (!canWithdraw(amount)) + throw new IllegalStateException("Account balance can't be negative"); + balance = balance.subtract(amount); + } + + ClosedAccount closedAccount() { + return new ClosedAccount(); + } + } + + public static class ClosedAccount implements Account {} + + public static Behavior behavior(String accountNumber) { + return Behaviors.setup(context -> new AccountEntity(context, accountNumber)); + } + + public AccountEntity(ActorContext context, String accountNumber) { + super(new PersistenceId(accountNumber)); + } + + @Override + public Account emptyState() { + return new EmptyAccount(); + } + + @Override + public CommandHandler commandHandler() { + CommandHandlerBuilder builder = + newCommandHandlerBuilder(); + + builder + .forStateType(EmptyAccount.class) + .matchCommand(CreateAccount.class, this::createAccount); + + builder + .forStateType(OpenedAccount.class) + .matchCommand(Deposit.class, this::deposit) + .matchCommand(Withdraw.class, this::withdraw) + .matchCommand(GetBalance.class, this::getBalance) + .matchCommand(CloseAccount.class, this::closeAccount); + + builder.forStateType(ClosedAccount.class).matchAny(() -> Effect().unhandled()); + + return builder.build(); + } + + private ReplyEffect createAccount( + EmptyAccount account, CreateAccount command) { + return Effect() + .persist(new AccountCreated()) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } + + private ReplyEffect deposit(OpenedAccount account, Deposit command) { + return Effect() + .persist(new Deposited(command.amount)) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } + + private ReplyEffect withdraw(OpenedAccount account, Withdraw command) { + if (!account.canWithdraw(command.amount)) { + return Effect() + .reply(command, new Rejected("not enough funds to withdraw " + command.amount)); + } else { + return Effect() + .persist(new Withdrawn(command.amount)) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } + } + + private ReplyEffect getBalance( + OpenedAccount account, GetBalance command) { + return Effect().reply(command, new CurrentBalance(account.balance)); + } + + private ReplyEffect closeAccount( + OpenedAccount account, CloseAccount command) { + if (account.getBalance().equals(BigDecimal.ZERO)) { + return Effect() + .persist(new AccountClosed()) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } else { + return Effect().reply(command, new Rejected("balance must be zero for closing account")); + } + } + + @Override + public EventHandler eventHandler() { + EventHandlerBuilder builder = newEventHandlerBuilder(); + + builder + .forStateType(EmptyAccount.class) + .matchEvent(AccountCreated.class, (account, event) -> account.openedAccount()); + + builder + .forStateType(OpenedAccount.class) + .matchEvent( + Deposited.class, + (account, deposited) -> { + account.makeDeposit(deposited.amount); + return account; + }) + .matchEvent( + Withdrawn.class, + (account, withdrawn) -> { + account.makeWithdraw(withdrawn.amount); + return account; + }) + .matchEvent(AccountClosed.class, (account, closed) -> account.closedAccount()); + + return builder.build(); + } + } + + // #account-entity +} diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithNullState.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithNullState.java new file mode 100644 index 0000000000..d5d5773626 --- /dev/null +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleWithNullState.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2018-2019 Lightbend Inc. + */ + +package jdocs.akka.persistence.typed; + +import akka.actor.typed.ActorRef; +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.ActorContext; +import akka.actor.typed.javadsl.Behaviors; +import akka.persistence.typed.ExpectingReply; +import akka.persistence.typed.PersistenceId; +import akka.persistence.typed.javadsl.CommandHandler; +import akka.persistence.typed.javadsl.CommandHandlerBuilder; +import akka.persistence.typed.javadsl.EventHandler; +import akka.persistence.typed.javadsl.EventHandlerBuilder; +import akka.persistence.typed.javadsl.EventSourcedBehaviorWithEnforcedReplies; +import akka.persistence.typed.javadsl.ReplyEffect; + +import java.math.BigDecimal; + +/** + * Bank account example illustrating: - different state classes representing the lifecycle of the + * account - null as emptyState - event handlers that delegate to methods in the state classes - + * command handlers that delegate to methods in the EventSourcedBehavior class - replies of various + * types, using ExpectingReply and EventSourcedBehaviorWithEnforcedReplies + */ +public interface AccountExampleWithNullState { + + // #account-entity + public class AccountEntity + extends EventSourcedBehaviorWithEnforcedReplies< + AccountEntity.AccountCommand, AccountEntity.AccountEvent, AccountEntity.Account> { + + // Command + interface AccountCommand extends ExpectingReply {} + + public static class CreateAccount implements AccountCommand { + private final ActorRef replyTo; + + public CreateAccount(ActorRef replyTo) { + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class Deposit implements AccountCommand { + public final BigDecimal amount; + private final ActorRef replyTo; + + public Deposit(BigDecimal amount, ActorRef replyTo) { + this.replyTo = replyTo; + this.amount = amount; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class Withdraw implements AccountCommand { + public final BigDecimal amount; + private final ActorRef replyTo; + + public Withdraw(BigDecimal amount, ActorRef replyTo) { + this.amount = amount; + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class GetBalance implements AccountCommand { + private final ActorRef replyTo; + + public GetBalance(ActorRef replyTo) { + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + public static class CloseAccount implements AccountCommand { + private final ActorRef replyTo; + + public CloseAccount(ActorRef replyTo) { + this.replyTo = replyTo; + } + + @Override + public ActorRef replyTo() { + return replyTo; + } + } + + // Reply + interface AccountCommandReply {} + + interface OperationResult extends AccountCommandReply {} + + enum Confirmed implements OperationResult { + INSTANCE + } + + public static class Rejected implements OperationResult { + public final String reason; + + public Rejected(String reason) { + this.reason = reason; + } + } + + public static class CurrentBalance implements AccountCommandReply { + public final BigDecimal balance; + + public CurrentBalance(BigDecimal balance) { + this.balance = balance; + } + } + + // Event + interface AccountEvent {} + + public static class AccountCreated implements AccountEvent {} + + public static class Deposited implements AccountEvent { + public final BigDecimal amount; + + Deposited(BigDecimal amount) { + this.amount = amount; + } + } + + public static class Withdrawn implements AccountEvent { + public final BigDecimal amount; + + Withdrawn(BigDecimal amount) { + this.amount = amount; + } + } + + public static class AccountClosed implements AccountEvent {} + + // State + interface Account {} + + public static class OpenedAccount implements Account { + public final BigDecimal balance; + + public OpenedAccount() { + this.balance = BigDecimal.ZERO; + } + + public OpenedAccount(BigDecimal balance) { + this.balance = balance; + } + + OpenedAccount makeDeposit(BigDecimal amount) { + return new OpenedAccount(balance.add(amount)); + } + + boolean canWithdraw(BigDecimal amount) { + return (balance.subtract(amount).compareTo(BigDecimal.ZERO) >= 0); + } + + OpenedAccount makeWithdraw(BigDecimal amount) { + if (!canWithdraw(amount)) + throw new IllegalStateException("Account balance can't be negative"); + return new OpenedAccount(balance.subtract(amount)); + } + + ClosedAccount closedAccount() { + return new ClosedAccount(); + } + } + + public static class ClosedAccount implements Account {} + + public static Behavior behavior(String accountNumber) { + return Behaviors.setup(context -> new AccountEntity(context, accountNumber)); + } + + public AccountEntity(ActorContext context, String accountNumber) { + super(new PersistenceId(accountNumber)); + } + + @Override + public Account emptyState() { + return null; + } + + @Override + public CommandHandler commandHandler() { + CommandHandlerBuilder builder = + newCommandHandlerBuilder(); + + builder.forNullState().matchCommand(CreateAccount.class, this::createAccount); + + builder + .forStateType(OpenedAccount.class) + .matchCommand(Deposit.class, this::deposit) + .matchCommand(Withdraw.class, this::withdraw) + .matchCommand(GetBalance.class, this::getBalance) + .matchCommand(CloseAccount.class, this::closeAccount); + + builder.forStateType(ClosedAccount.class).matchAny(() -> Effect().unhandled()); + + return builder.build(); + } + + private ReplyEffect createAccount(CreateAccount command) { + return Effect() + .persist(new AccountCreated()) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } + + private ReplyEffect deposit(OpenedAccount account, Deposit command) { + return Effect() + .persist(new Deposited(command.amount)) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } + + private ReplyEffect withdraw(OpenedAccount account, Withdraw command) { + if (!account.canWithdraw(command.amount)) { + return Effect() + .reply(command, new Rejected("not enough funds to withdraw " + command.amount)); + } else { + return Effect() + .persist(new Withdrawn(command.amount)) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } + } + + private ReplyEffect getBalance( + OpenedAccount account, GetBalance command) { + return Effect().reply(command, new CurrentBalance(account.balance)); + } + + private ReplyEffect closeAccount( + OpenedAccount account, CloseAccount command) { + if (account.balance.equals(BigDecimal.ZERO)) { + return Effect() + .persist(new AccountClosed()) + .thenReply(command, account2 -> Confirmed.INSTANCE); + } else { + return Effect().reply(command, new Rejected("balance must be zero for closing account")); + } + } + + @Override + public EventHandler eventHandler() { + EventHandlerBuilder builder = newEventHandlerBuilder(); + + builder.forNullState().matchEvent(AccountCreated.class, () -> new OpenedAccount()); + + builder + .forStateType(OpenedAccount.class) + .matchEvent( + Deposited.class, + (account, deposited) -> { + account.makeDeposit(deposited.amount); + return account; + }) + .matchEvent( + Withdrawn.class, + (account, withdrawn) -> { + account.makeWithdraw(withdrawn.amount); + return account; + }) + .matchEvent(AccountClosed.class, (account, closed) -> account.closedAccount()); + + return builder.build(); + } + } + + // #account-entity +} diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/BasicPersistentBehaviorTest.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/BasicPersistentBehaviorTest.java index 853f4017cd..dd193271e1 100644 --- a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/BasicPersistentBehaviorTest.java +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/BasicPersistentBehaviorTest.java @@ -6,6 +6,7 @@ package jdocs.akka.persistence.typed; import akka.actor.typed.Behavior; import akka.actor.typed.SupervisorStrategy; +import akka.actor.typed.javadsl.ActorContext; import akka.actor.typed.javadsl.Behaviors; import akka.persistence.typed.PersistenceId; import akka.persistence.typed.javadsl.CommandHandler; @@ -13,78 +14,260 @@ import akka.persistence.typed.javadsl.EventHandler; import akka.persistence.typed.javadsl.EventSourcedBehavior; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.Set; public class BasicPersistentBehaviorTest { - // #structure - public interface Command {} + interface Structure { + // #structure + public class MyPersistentBehavior + extends EventSourcedBehavior< + MyPersistentBehavior.Command, MyPersistentBehavior.Event, MyPersistentBehavior.State> { - public interface Event {} + static EventSourcedBehavior eventSourcedBehavior = + new MyPersistentBehavior(new PersistenceId("pid")); - public static class State {} + interface Command {} - // #supervision - public static class MyPersistentBehavior extends EventSourcedBehavior { - public MyPersistentBehavior(PersistenceId persistenceId) { - super( - persistenceId, - SupervisorStrategy.restartWithBackoff( - Duration.ofSeconds(10), Duration.ofSeconds(30), 0.2)); + interface Event {} + + public static class State {} + + public MyPersistentBehavior(PersistenceId persistenceId) { + super(persistenceId); + } + + @Override + public State emptyState() { + return new State(); + } + + @Override + public CommandHandler commandHandler() { + return (state, command) -> { + throw new RuntimeException("TODO: process the command & return an Effect"); + }; + } + + @Override + public EventHandler eventHandler() { + return (state, event) -> { + throw new RuntimeException("TODO: process the event return the next state"); + }; + } } - // #supervision - - @Override - public State emptyState() { - return new State(); - } - - @Override - public CommandHandler commandHandler() { - return (state, command) -> { - throw new RuntimeException("TODO: process the command & return an Effect"); - }; - } - - @Override - public EventHandler eventHandler() { - return (state, event) -> { - throw new RuntimeException("TODO: process the event return the next state"); - }; - } - - // #recovery - @Override - public void onRecoveryCompleted(State state) { - throw new RuntimeException("TODO: add some end-of-recovery side-effect here"); - } - // #recovery - - // #tagging - @Override - public Set tagsFor(Event event) { - throw new RuntimeException("TODO: inspect the event and return any tags it should have"); - } - // #tagging + // #structure } - static EventSourcedBehavior eventSourcedBehavior = - new MyPersistentBehavior(new PersistenceId("pid")); - // #structure + interface FirstExample { + // #behavior + public class MyPersistentBehavior + extends EventSourcedBehavior< + MyPersistentBehavior.Command, MyPersistentBehavior.Event, MyPersistentBehavior.State> { - // #wrapPersistentBehavior - static Behavior debugAlwaysSnapshot = - Behaviors.setup( - (context) -> { - return new MyPersistentBehavior(new PersistenceId("pid")) { - @Override - public boolean shouldSnapshot(State state, Event event, long sequenceNr) { - context - .getLog() - .info("Snapshot actor {} => state: {}", context.getSelf().path().name(), state); - return true; - } - }; - }); - // #wrapPersistentBehavior + // #behavior + + // #command + interface Command {} + + public static class Add implements Command { + public final String data; + + public Add(String data) { + this.data = data; + } + } + + public enum Clear implements Command { + INSTANCE + } + + interface Event {} + + public static class Added implements Event { + public final String data; + + public Added(String data) { + this.data = data; + } + } + + public enum Cleared implements Event { + INSTANCE + } + // #command + + // #state + public static class State { + private final List items; + + private State(List items) { + this.items = items; + } + + public State() { + this.items = new ArrayList<>(); + } + + public State addItem(String data) { + List newItems = new ArrayList<>(items); + newItems.add(0, data); + // keep 5 items + List latest = newItems.subList(0, Math.min(4, newItems.size() - 1)); + return new State(latest); + } + } + // #state + + // #behavior + public MyPersistentBehavior(PersistenceId persistenceId) { + super(persistenceId); + } + + @Override + public State emptyState() { + return new State(); + } + + // #command-handler + @Override + public CommandHandler commandHandler() { + return newCommandHandlerBuilder() + .forAnyState() + .matchCommand(Add.class, command -> Effect().persist(new Added(command.data))) + .matchCommand(Clear.class, command -> Effect().persist(Cleared.INSTANCE)) + .build(); + } + // #command-handler + + // #event-handler + @Override + public EventHandler eventHandler() { + return newEventHandlerBuilder() + .forAnyState() + .matchEvent(Added.class, (state, event) -> state.addItem(event.data)) + .matchEvent(Cleared.class, () -> new State()) + .build(); + } + // #event-handler + } + // #behavior + + } + + interface More { + interface Command {} + + interface Event {} + + public static class State {} + + // #supervision + public class MyPersistentBehavior extends EventSourcedBehavior { + public MyPersistentBehavior(PersistenceId persistenceId) { + super( + persistenceId, + SupervisorStrategy.restartWithBackoff( + Duration.ofSeconds(10), Duration.ofSeconds(30), 0.2)); + } + // #supervision + + @Override + public State emptyState() { + return new State(); + } + + @Override + public CommandHandler commandHandler() { + return (state, command) -> { + throw new RuntimeException("TODO: process the command & return an Effect"); + }; + } + + @Override + public EventHandler eventHandler() { + return (state, event) -> { + throw new RuntimeException("TODO: process the event return the next state"); + }; + } + + // #recovery + @Override + public void onRecoveryCompleted(State state) { + throw new RuntimeException("TODO: add some end-of-recovery side-effect here"); + } + // #recovery + + // #tagging + @Override + public Set tagsFor(Event event) { + throw new RuntimeException("TODO: inspect the event and return any tags it should have"); + } + // #tagging + } + + EventSourcedBehavior eventSourcedBehavior = + new MyPersistentBehavior(new PersistenceId("pid")); + + // #wrapPersistentBehavior + Behavior debugAlwaysSnapshot = + Behaviors.setup( + (context) -> { + return new MyPersistentBehavior(new PersistenceId("pid")) { + @Override + public boolean shouldSnapshot(State state, Event event, long sequenceNr) { + context + .getLog() + .info( + "Snapshot actor {} => state: {}", context.getSelf().path().name(), state); + return true; + } + }; + }); + // #wrapPersistentBehavior + } + + interface WithActorContext { + interface Command {} + + interface Event {} + + public static class State {} + + // #actor-context + public class MyPersistentBehavior extends EventSourcedBehavior { + + public static Behavior behavior(PersistenceId persistenceId) { + return Behaviors.setup(ctx -> new MyPersistentBehavior(persistenceId, ctx)); + } + + // this makes the context available to the command handler etc. + private final ActorContext ctx; + + public MyPersistentBehavior(PersistenceId persistenceId, ActorContext ctx) { + super(persistenceId); + this.ctx = ctx; + } + + // #actor-context + @Override + public State emptyState() { + return null; + } + + @Override + public CommandHandler commandHandler() { + return null; + } + + @Override + public EventHandler eventHandler() { + return null; + } + // #actor-context + } + // #actor-context + } } diff --git a/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorCompileOnlyTest.scala b/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorCompileOnlyTest.scala index ed11863484..e92df225f0 100644 --- a/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorCompileOnlyTest.scala +++ b/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorCompileOnlyTest.scala @@ -17,43 +17,6 @@ object PersistentActorCompileOnlyTest { import akka.persistence.typed.scaladsl.EventSourcedBehavior._ - object Simple { - //#command - sealed trait SimpleCommand - case class Cmd(data: String) extends SimpleCommand - - sealed trait SimpleEvent - case class Evt(data: String) extends SimpleEvent - //#command - - //#state - case class ExampleState(events: List[String] = Nil) - //#state - - //#command-handler - val commandHandler: CommandHandler[SimpleCommand, SimpleEvent, ExampleState] = - CommandHandler.command { - case Cmd(data) ⇒ Effect.persist(Evt(data)) - } - //#command-handler - - //#event-handler - val eventHandler: (ExampleState, SimpleEvent) ⇒ ExampleState = { - case (state, Evt(data)) ⇒ state.copy(data :: state.events) - } - //#event-handler - - //#behavior - val simpleBehavior: EventSourcedBehavior[SimpleCommand, SimpleEvent, ExampleState] = - EventSourcedBehavior[SimpleCommand, SimpleEvent, ExampleState]( - persistenceId = PersistenceId("sample-id-1"), - emptyState = ExampleState(Nil), - commandHandler = commandHandler, - eventHandler = eventHandler) - //#behavior - - } - object WithAck { case object Ack @@ -434,30 +397,4 @@ object PersistentActorCompileOnlyTest { } - object WithContext { - sealed trait Command - sealed trait Event - class State - - // #actor-context - val behavior: Behavior[String] = - Behaviors.setup { ctx ⇒ - EventSourcedBehavior[String, String, State]( - persistenceId = PersistenceId("myPersistenceId"), - emptyState = new State, - commandHandler = CommandHandler.command { - cmd ⇒ - ctx.log.info("Got command {}", cmd) - Effect.persist(cmd).thenRun { state ⇒ - ctx.log.info("event persisted, new state {}", state) - } - }, - eventHandler = { - case (state, _) ⇒ state - }) - } - // #actor-context - - } - } diff --git a/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/BasicPersistentBehaviorCompileOnly.scala b/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/BasicPersistentBehaviorCompileOnly.scala index 7b40971fe7..62e79576de 100644 --- a/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/BasicPersistentBehaviorCompileOnly.scala +++ b/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/BasicPersistentBehaviorCompileOnly.scala @@ -14,10 +14,58 @@ import akka.persistence.typed.PersistenceId object BasicPersistentBehaviorCompileOnly { + object FirstExample { + //#command + sealed trait Command + final case class Add(data: String) extends Command + case object Clear extends Command + + sealed trait Event + final case class Added(data: String) extends Event + case object Cleared extends Event + //#command + + //#state + final case class State(history: List[String] = Nil) + //#state + + //#command-handler + import akka.persistence.typed.scaladsl.Effect + + val commandHandler: (State, Command) ⇒ Effect[Event, State] = { + (state, command) ⇒ + command match { + case Add(data) ⇒ Effect.persist(Added(data)) + case Clear ⇒ Effect.persist(Cleared) + } + } + //#command-handler + + //#event-handler + val eventHandler: (State, Event) ⇒ State = { + (state, event) ⇒ + event match { + case Added(data) ⇒ state.copy((data :: state.history).take(5)) + case Cleared ⇒ State(Nil) + } + } + //#event-handler + + //#behavior + def behavior(id: String): EventSourcedBehavior[Command, Event, State] = + EventSourcedBehavior[Command, Event, State]( + persistenceId = PersistenceId(id), + emptyState = State(Nil), + commandHandler = commandHandler, + eventHandler = eventHandler) + //#behavior + + } + //#structure sealed trait Command sealed trait Event - case class State() + final case class State() val behavior: Behavior[Command] = EventSourcedBehavior[Command, Event, State]( @@ -32,9 +80,6 @@ object BasicPersistentBehaviorCompileOnly { ) //#structure - case class CommandWithSender(reply: ActorRef[String]) extends Command - case class VeryImportantEvent() extends Event - //#recovery val recoveryBehavior: Behavior[Command] = EventSourcedBehavior[Command, Event, State]( @@ -99,4 +144,26 @@ object BasicPersistentBehaviorCompileOnly { )) //#supervision + // #actor-context + import akka.persistence.typed.scaladsl.Effect + import akka.persistence.typed.scaladsl.EventSourcedBehavior.CommandHandler + + val behaviorWithContext: Behavior[String] = + Behaviors.setup { context ⇒ + EventSourcedBehavior[String, String, State]( + persistenceId = PersistenceId("myPersistenceId"), + emptyState = new State, + commandHandler = CommandHandler.command { + cmd ⇒ + context.log.info("Got command {}", cmd) + Effect.persist(cmd).thenRun { state ⇒ + context.log.info("event persisted, new state {}", state) + } + }, + eventHandler = { + case (state, _) ⇒ state + }) + } + // #actor-context + }