null as empty state, in javadsl, #25768
This commit is contained in:
parent
1691961a10
commit
bed17cc172
9 changed files with 568 additions and 16 deletions
|
|
@ -108,7 +108,11 @@ private[akka] trait EventsourcedJournalInteractions[C, E, S] {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected def internalSaveSnapshot(state: EventsourcedRunning.EventsourcedState[S]): Unit = {
|
protected def internalSaveSnapshot(state: EventsourcedRunning.EventsourcedState[S]): Unit = {
|
||||||
setup.snapshotStore.tell(SnapshotProtocol.SaveSnapshot(SnapshotMetadata(setup.persistenceId.id, state.seqNr), state.state), setup.selfUntyped)
|
// don't store null state
|
||||||
|
if (state.state != null)
|
||||||
|
setup.snapshotStore.tell(SnapshotProtocol.SaveSnapshot(
|
||||||
|
SnapshotMetadata(setup.persistenceId.id, state.seqNr),
|
||||||
|
state.state), setup.selfUntyped)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ package akka.persistence.typed.javadsl
|
||||||
|
|
||||||
import java.util.function.BiFunction
|
import java.util.function.BiFunction
|
||||||
import java.util.function.Predicate
|
import java.util.function.Predicate
|
||||||
|
import java.util.function.{ Function ⇒ JFunction }
|
||||||
|
|
||||||
import akka.annotation.InternalApi
|
import akka.annotation.InternalApi
|
||||||
import akka.persistence.typed.internal._
|
import akka.persistence.typed.internal._
|
||||||
|
|
@ -23,18 +24,14 @@ trait CommandHandler[Command, Event, State] {
|
||||||
|
|
||||||
object CommandHandlerBuilder {
|
object CommandHandlerBuilder {
|
||||||
|
|
||||||
private val _trueStatePredicate: Predicate[Any] = new Predicate[Any] {
|
|
||||||
override def test(t: Any): Boolean = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private def trueStatePredicate[S]: Predicate[S] = _trueStatePredicate.asInstanceOf[Predicate[S]]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param stateClass The handlers defined by this builder are used when the state is an instance of the `stateClass`
|
* @param stateClass The handlers defined by this builder are used when the state is an instance of the `stateClass`
|
||||||
* @return A new, mutable, command handler builder
|
* @return A new, mutable, command handler builder
|
||||||
*/
|
*/
|
||||||
def builder[Command, Event, S <: State, State](stateClass: Class[S]): CommandHandlerBuilder[Command, Event, S, State] =
|
def builder[Command, Event, S <: State, State](stateClass: Class[S]): CommandHandlerBuilder[Command, Event, S, State] =
|
||||||
new CommandHandlerBuilder(stateClass, statePredicate = trueStatePredicate)
|
new CommandHandlerBuilder(statePredicate = new Predicate[S] {
|
||||||
|
override def test(state: S): Boolean = state != null && stateClass.isAssignableFrom(state.getClass)
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param statePredicate The handlers defined by this builder are used when the `statePredicate` is `true`,
|
* @param statePredicate The handlers defined by this builder are used when the `statePredicate` is `true`,
|
||||||
|
|
@ -42,7 +39,7 @@ object CommandHandlerBuilder {
|
||||||
* @return A new, mutable, command handler builder
|
* @return A new, mutable, command handler builder
|
||||||
*/
|
*/
|
||||||
def builder[Command, Event, State](statePredicate: Predicate[State]): CommandHandlerBuilder[Command, Event, State, State] =
|
def builder[Command, Event, State](statePredicate: Predicate[State]): CommandHandlerBuilder[Command, Event, State, State] =
|
||||||
new CommandHandlerBuilder(classOf[Any].asInstanceOf[Class[State]], statePredicate)
|
new CommandHandlerBuilder(statePredicate)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* INTERNAL API
|
* INTERNAL API
|
||||||
|
|
@ -54,7 +51,7 @@ object CommandHandlerBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
final class CommandHandlerBuilder[Command, Event, S <: State, State] @InternalApi private[persistence] (
|
final class CommandHandlerBuilder[Command, Event, S <: State, State] @InternalApi private[persistence] (
|
||||||
val stateClass: Class[S], val statePredicate: Predicate[S]) {
|
val statePredicate: Predicate[S]) {
|
||||||
import CommandHandlerBuilder.CommandHandlerCase
|
import CommandHandlerBuilder.CommandHandlerCase
|
||||||
|
|
||||||
private var cases: List[CommandHandlerCase[Command, Event, State]] = Nil
|
private var cases: List[CommandHandlerCase[Command, Event, State]] = Nil
|
||||||
|
|
@ -62,7 +59,7 @@ final class CommandHandlerBuilder[Command, Event, S <: State, State] @InternalAp
|
||||||
private def addCase(predicate: Command ⇒ Boolean, handler: BiFunction[S, Command, Effect[Event, State]]): Unit = {
|
private def addCase(predicate: Command ⇒ Boolean, handler: BiFunction[S, Command, Effect[Event, State]]): Unit = {
|
||||||
cases = CommandHandlerCase[Command, Event, State](
|
cases = CommandHandlerCase[Command, Event, State](
|
||||||
commandPredicate = predicate,
|
commandPredicate = predicate,
|
||||||
statePredicate = state ⇒ stateClass.isAssignableFrom(state.getClass) && statePredicate.test(state.asInstanceOf[S]),
|
statePredicate = state ⇒ statePredicate.test(state.asInstanceOf[S]),
|
||||||
handler.asInstanceOf[BiFunction[State, Command, Effect[Event, State]]]) :: cases
|
handler.asInstanceOf[BiFunction[State, Command, Effect[Event, State]]]) :: cases
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,17 +71,45 @@ final class CommandHandlerBuilder[Command, Event, S <: State, State] @InternalAp
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match any command which the given `predicate` returns true for.
|
||||||
|
*
|
||||||
|
* Use this when then `State` is not needed in the `handler`, otherwise there is an overloaded method that pass
|
||||||
|
* the state in a `BiFunction`.
|
||||||
|
*/
|
||||||
|
def matchCommand(predicate: Predicate[Command], handler: JFunction[Command, Effect[Event, State]]): CommandHandlerBuilder[Command, Event, S, State] = {
|
||||||
|
addCase(cmd ⇒ predicate.test(cmd), new BiFunction[S, Command, Effect[Event, State]] {
|
||||||
|
override def apply(state: S, cmd: Command): Effect[Event, State] = handler(cmd)
|
||||||
|
})
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match commands that are of the given `commandClass` or subclass thereof
|
||||||
|
*/
|
||||||
def matchCommand[C <: Command](commandClass: Class[C], handler: BiFunction[S, C, Effect[Event, State]]): CommandHandlerBuilder[Command, Event, S, State] = {
|
def matchCommand[C <: Command](commandClass: Class[C], handler: BiFunction[S, C, Effect[Event, State]]): CommandHandlerBuilder[Command, Event, S, State] = {
|
||||||
addCase(cmd ⇒ commandClass.isAssignableFrom(cmd.getClass), handler.asInstanceOf[BiFunction[S, Command, Effect[Event, State]]])
|
addCase(cmd ⇒ commandClass.isAssignableFrom(cmd.getClass), handler.asInstanceOf[BiFunction[S, Command, Effect[Event, State]]])
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match commands that are of the given `commandClass` or subclass thereof.
|
||||||
|
*
|
||||||
|
* Use this when then `State` is not needed in the `handler`, otherwise there is an overloaded method that pass
|
||||||
|
* the state in a `BiFunction`.
|
||||||
|
*/
|
||||||
|
def matchCommand[C <: Command](commandClass: Class[C], handler: JFunction[C, Effect[Event, State]]): CommandHandlerBuilder[Command, Event, S, State] = {
|
||||||
|
matchCommand[C](commandClass, new BiFunction[S, C, Effect[Event, State]] {
|
||||||
|
override def apply(state: S, cmd: C): Effect[Event, State] = handler(cmd)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compose this builder with another builder. The handlers in this builder will be tried first followed
|
* Compose this builder with another builder. The handlers in this builder will be tried first followed
|
||||||
* by the handlers in `other`.
|
* by the handlers in `other`.
|
||||||
*/
|
*/
|
||||||
def orElse[S2 <: State](other: CommandHandlerBuilder[Command, Event, S2, State]): CommandHandlerBuilder[Command, Event, S2, State] = {
|
def orElse[S2 <: State](other: CommandHandlerBuilder[Command, Event, S2, State]): CommandHandlerBuilder[Command, Event, S2, State] = {
|
||||||
val newBuilder = new CommandHandlerBuilder[Command, Event, S2, State](other.stateClass, other.statePredicate)
|
val newBuilder = new CommandHandlerBuilder[Command, Event, S2, State](other.statePredicate)
|
||||||
// problem with overloaded constructor with `cases` as parameter
|
// problem with overloaded constructor with `cases` as parameter
|
||||||
newBuilder.cases = other.cases ::: cases
|
newBuilder.cases = other.cases ::: cases
|
||||||
newBuilder
|
newBuilder
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
package akka.persistence.typed.javadsl
|
package akka.persistence.typed.javadsl
|
||||||
|
|
||||||
import java.util.function.BiFunction
|
import java.util.function.BiFunction
|
||||||
|
import java.util.function.{ Function ⇒ JFunction }
|
||||||
|
|
||||||
import akka.annotation.InternalApi
|
import akka.annotation.InternalApi
|
||||||
import akka.util.OptionVal
|
import akka.util.OptionVal
|
||||||
|
|
@ -49,11 +50,23 @@ final class EventHandlerBuilder[State >: Null, Event]() {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match any event which is an instance of `E` or a subtype of `E`.
|
||||||
|
*
|
||||||
|
* Use this when then `State` is not needed in the `handler`, otherwise there is an overloaded method that pass
|
||||||
|
* the state in a `BiFunction`.
|
||||||
|
*/
|
||||||
|
def matchEvent[E <: Event](eventClass: Class[E], f: JFunction[E, State]): EventHandlerBuilder[State, Event] = {
|
||||||
|
matchEvent[E](eventClass, new BiFunction[State, E, State] {
|
||||||
|
override def apply(state: State, event: E): State = f(event)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
def matchEvent[E <: Event, S <: State](eventClass: Class[E], stateClass: Class[S],
|
def matchEvent[E <: Event, S <: State](eventClass: Class[E], stateClass: Class[S],
|
||||||
biFunction: BiFunction[S, E, State]): EventHandlerBuilder[State, Event] = {
|
biFunction: BiFunction[S, E, State]): EventHandlerBuilder[State, Event] = {
|
||||||
|
|
||||||
cases = EventHandlerCase[State, Event](
|
cases = EventHandlerCase[State, Event](
|
||||||
statePredicate = s ⇒ stateClass.isAssignableFrom(s.getClass),
|
statePredicate = s ⇒ s != null && stateClass.isAssignableFrom(s.getClass),
|
||||||
eventPredicate = e ⇒ eventClass.isAssignableFrom(e.getClass),
|
eventPredicate = e ⇒ eventClass.isAssignableFrom(e.getClass),
|
||||||
biFunction.asInstanceOf[BiFunction[State, Event, State]]) :: cases
|
biFunction.asInstanceOf[BiFunction[State, Event, State]]) :: cases
|
||||||
this
|
this
|
||||||
|
|
@ -102,7 +115,9 @@ final class EventHandlerBuilder[State >: Null, Event]() {
|
||||||
}
|
}
|
||||||
|
|
||||||
result match {
|
result match {
|
||||||
case OptionVal.None ⇒ throw new MatchError(s"No match found for event [${event.getClass}] and state [${state.getClass}]. Has this event been stored using an EventAdapter?")
|
case OptionVal.None ⇒
|
||||||
|
val stateClass = if (state == null) "null" else state.getClass.getName
|
||||||
|
throw new MatchError(s"No match found for event [${event.getClass}] and state [$stateClass]. Has this event been stored using an EventAdapter?")
|
||||||
case OptionVal.Some(s) ⇒ s
|
case OptionVal.Some(s) ⇒ s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.persistence.typed.javadsl;
|
||||||
|
|
||||||
|
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
|
||||||
|
import akka.actor.testkit.typed.javadsl.TestProbe;
|
||||||
|
import akka.actor.typed.ActorRef;
|
||||||
|
import akka.actor.typed.Behavior;
|
||||||
|
import akka.actor.typed.javadsl.Behaviors;
|
||||||
|
import akka.persistence.typed.PersistenceId;
|
||||||
|
import com.typesafe.config.Config;
|
||||||
|
import com.typesafe.config.ConfigFactory;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.scalatest.junit.JUnitSuite;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class NullEmptyStateTest extends JUnitSuite {
|
||||||
|
|
||||||
|
private static final Config config = ConfigFactory.parseString(
|
||||||
|
"akka.persistence.journal.plugin = \"akka.persistence.journal.inmem\" \n");
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static final TestKitJunitResource testKit = new TestKitJunitResource(config);
|
||||||
|
|
||||||
|
static class NullEmptyState extends PersistentBehavior<String, String, String> {
|
||||||
|
|
||||||
|
private final ActorRef<String> probe;
|
||||||
|
|
||||||
|
NullEmptyState(PersistenceId persistenceId, ActorRef<String> probe) {
|
||||||
|
super(persistenceId);
|
||||||
|
this.probe = probe;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String emptyState() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRecoveryCompleted(String s) {
|
||||||
|
probe.tell("onRecoveryCompleted:" + s);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandHandler<String, String, String> commandHandler() {
|
||||||
|
CommandHandlerBuilder<String, String, String, String> b1 =
|
||||||
|
commandHandlerBuilder(Objects::isNull)
|
||||||
|
.matchCommand("stop"::equals, command -> Effect().stop())
|
||||||
|
.matchCommand(String.class, this::persistCommand);
|
||||||
|
|
||||||
|
CommandHandlerBuilder<String, String, String, String> b2 =
|
||||||
|
commandHandlerBuilder(String.class)
|
||||||
|
.matchCommand("stop"::equals, command -> Effect().stop())
|
||||||
|
.matchCommand(String.class, this::persistCommand);
|
||||||
|
|
||||||
|
return b1.orElse(b2).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Effect<String, String> persistCommand(String command) {
|
||||||
|
return Effect().persist(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EventHandler<String, String> eventHandler() {
|
||||||
|
return eventHandlerBuilder()
|
||||||
|
.matchEvent(String.class, this::applyEvent)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String applyEvent(String state, String event) {
|
||||||
|
probe.tell("eventHandler:" + state + ":" + event);
|
||||||
|
if (state == null)
|
||||||
|
return event;
|
||||||
|
else
|
||||||
|
return state + event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void handleNullState() throws Exception {
|
||||||
|
TestProbe<String> probe = testKit.createTestProbe();
|
||||||
|
Behavior<String> b = Behaviors.setup(ctx -> new NullEmptyState(new PersistenceId("a"), probe.ref()));
|
||||||
|
|
||||||
|
ActorRef<String> ref1 = testKit.spawn(b);
|
||||||
|
probe.expectMessage("onRecoveryCompleted:null");
|
||||||
|
ref1.tell("stop");
|
||||||
|
|
||||||
|
ActorRef<String> ref2 = testKit.spawn(b);
|
||||||
|
probe.expectMessage("onRecoveryCompleted:null");
|
||||||
|
ref2.tell("one");
|
||||||
|
probe.expectMessage("eventHandler:null:one");
|
||||||
|
ref2.tell("two");
|
||||||
|
probe.expectMessage("eventHandler:one:two");
|
||||||
|
|
||||||
|
ref2.tell("stop");
|
||||||
|
ActorRef<String> ref3 = testKit.spawn(b);
|
||||||
|
// eventHandler from reply
|
||||||
|
probe.expectMessage("eventHandler:null:one");
|
||||||
|
probe.expectMessage("eventHandler:one:two");
|
||||||
|
probe.expectMessage("onRecoveryCompleted:onetwo");
|
||||||
|
ref3.tell("three");
|
||||||
|
probe.expectMessage("eventHandler:onetwo:three");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.persistence.typed.javadsl;
|
||||||
|
|
||||||
|
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
|
||||||
|
import akka.actor.testkit.typed.javadsl.TestProbe;
|
||||||
|
import akka.actor.typed.ActorRef;
|
||||||
|
import akka.actor.typed.Behavior;
|
||||||
|
import akka.actor.typed.javadsl.Behaviors;
|
||||||
|
import akka.persistence.typed.PersistenceId;
|
||||||
|
import com.typesafe.config.Config;
|
||||||
|
import com.typesafe.config.ConfigFactory;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.scalatest.junit.JUnitSuite;
|
||||||
|
|
||||||
|
public class PrimitiveStateTest extends JUnitSuite {
|
||||||
|
|
||||||
|
private static final Config config = ConfigFactory.parseString(
|
||||||
|
"akka.persistence.journal.plugin = \"akka.persistence.journal.inmem\" \n");
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static final TestKitJunitResource testKit = new TestKitJunitResource(config);
|
||||||
|
|
||||||
|
static class PrimitiveState extends PersistentBehavior<Integer, Integer, Integer> {
|
||||||
|
|
||||||
|
private final ActorRef<String> probe;
|
||||||
|
|
||||||
|
PrimitiveState(PersistenceId persistenceId, ActorRef<String> probe) {
|
||||||
|
super(persistenceId);
|
||||||
|
this.probe = probe;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer emptyState() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRecoveryCompleted(Integer n) {
|
||||||
|
probe.tell("onRecoveryCompleted:" + n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandHandler<Integer, Integer, Integer> commandHandler() {
|
||||||
|
return (state, command) -> {
|
||||||
|
if (command < 0)
|
||||||
|
return Effect().stop();
|
||||||
|
else
|
||||||
|
return Effect().persist(command);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EventHandler<Integer, Integer> eventHandler() {
|
||||||
|
return (state, event) -> {
|
||||||
|
probe.tell("eventHandler:" + state + ":" + event);
|
||||||
|
return state + event;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void handleIntegerState() throws Exception {
|
||||||
|
TestProbe<String> probe = testKit.createTestProbe();
|
||||||
|
Behavior<Integer> b = Behaviors.setup(ctx -> new PrimitiveState(new PersistenceId("a"), probe.ref()));
|
||||||
|
ActorRef<Integer> ref1 = testKit.spawn(b);
|
||||||
|
probe.expectMessage("onRecoveryCompleted:0");
|
||||||
|
ref1.tell(1);
|
||||||
|
probe.expectMessage("eventHandler:0:1");
|
||||||
|
ref1.tell(2);
|
||||||
|
probe.expectMessage("eventHandler:1:2");
|
||||||
|
|
||||||
|
ref1.tell(-1);
|
||||||
|
ActorRef<Integer> ref2 = testKit.spawn(b);
|
||||||
|
// eventHandler from reply
|
||||||
|
probe.expectMessage("eventHandler:0:1");
|
||||||
|
probe.expectMessage("eventHandler:1:2");
|
||||||
|
probe.expectMessage("onRecoveryCompleted:3");
|
||||||
|
ref2.tell(3);
|
||||||
|
probe.expectMessage("eventHandler:3:3");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package jdocs.akka.persistence.typed;
|
||||||
|
|
||||||
|
import akka.Done;
|
||||||
|
import akka.actor.typed.ActorRef;
|
||||||
|
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.PersistentBehavior;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class NullBlogState {
|
||||||
|
|
||||||
|
interface BlogEvent {
|
||||||
|
}
|
||||||
|
public static class PostAdded implements BlogEvent {
|
||||||
|
private final String postId;
|
||||||
|
private final PostContent content;
|
||||||
|
|
||||||
|
public PostAdded(String postId, PostContent content) {
|
||||||
|
this.postId = postId;
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BodyChanged implements BlogEvent {
|
||||||
|
private final String postId;
|
||||||
|
private final String newBody;
|
||||||
|
|
||||||
|
public BodyChanged(String postId, String newBody) {
|
||||||
|
this.postId = postId;
|
||||||
|
this.newBody = newBody;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Published implements BlogEvent {
|
||||||
|
private final String postId;
|
||||||
|
|
||||||
|
public Published(String postId) {
|
||||||
|
this.postId = postId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BlogState {
|
||||||
|
final PostContent postContent;
|
||||||
|
final boolean published;
|
||||||
|
|
||||||
|
BlogState(PostContent postContent, boolean published) {
|
||||||
|
this.postContent = postContent;
|
||||||
|
this.published = published;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlogState withContent(PostContent newContent) {
|
||||||
|
return new BlogState(newContent, this.published);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String postId() {
|
||||||
|
return postContent.postId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface BlogCommand {
|
||||||
|
}
|
||||||
|
public static class AddPost implements BlogCommand {
|
||||||
|
final PostContent content;
|
||||||
|
final ActorRef<AddPostDone> replyTo;
|
||||||
|
|
||||||
|
public AddPost(PostContent content, ActorRef<AddPostDone> replyTo) {
|
||||||
|
this.content = content;
|
||||||
|
this.replyTo = replyTo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static class AddPostDone implements BlogCommand {
|
||||||
|
final String postId;
|
||||||
|
|
||||||
|
public AddPostDone(String postId) {
|
||||||
|
this.postId = postId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static class GetPost implements BlogCommand {
|
||||||
|
final ActorRef<PostContent> replyTo;
|
||||||
|
|
||||||
|
public GetPost(ActorRef<PostContent> replyTo) {
|
||||||
|
this.replyTo = replyTo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static class ChangeBody implements BlogCommand {
|
||||||
|
final String newBody;
|
||||||
|
final ActorRef<Done> replyTo;
|
||||||
|
|
||||||
|
public ChangeBody(String newBody, ActorRef<Done> replyTo) {
|
||||||
|
this.newBody = newBody;
|
||||||
|
this.replyTo = replyTo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static class Publish implements BlogCommand {
|
||||||
|
final ActorRef<Done> replyTo;
|
||||||
|
|
||||||
|
public Publish(ActorRef<Done> replyTo) {
|
||||||
|
this.replyTo = replyTo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static class PassivatePost implements BlogCommand {
|
||||||
|
|
||||||
|
}
|
||||||
|
public static class PostContent implements BlogCommand {
|
||||||
|
final String postId;
|
||||||
|
final String title;
|
||||||
|
final String body;
|
||||||
|
|
||||||
|
public PostContent(String postId, String title, String body) {
|
||||||
|
this.postId = postId;
|
||||||
|
this.title = title;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BlogBehavior extends PersistentBehavior<BlogCommand, BlogEvent, BlogState> {
|
||||||
|
|
||||||
|
private CommandHandlerBuilder<BlogCommand, BlogEvent, BlogState, BlogState> initialCommandHandler() {
|
||||||
|
return commandHandlerBuilder(Objects::isNull)
|
||||||
|
.matchCommand(AddPost.class, cmd -> {
|
||||||
|
PostAdded event = new PostAdded(cmd.content.postId, cmd.content);
|
||||||
|
return Effect().persist(event)
|
||||||
|
.andThen(() -> cmd.replyTo.tell(new AddPostDone(cmd.content.postId)));
|
||||||
|
})
|
||||||
|
.matchCommand(PassivatePost.class, cmd -> Effect().stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
private CommandHandlerBuilder<BlogCommand, BlogEvent, BlogState, BlogState> postCommandHandler() {
|
||||||
|
return commandHandlerBuilder(Objects::nonNull)
|
||||||
|
.matchCommand(ChangeBody.class, (state, cmd) -> {
|
||||||
|
BodyChanged event = new BodyChanged(state.postId(), cmd.newBody);
|
||||||
|
return Effect().persist(event).andThen(() -> cmd.replyTo.tell(Done.getInstance()));
|
||||||
|
})
|
||||||
|
.matchCommand(Publish.class, (state, cmd) -> Effect()
|
||||||
|
.persist(new Published(state.postId())).andThen(() -> {
|
||||||
|
System.out.println("Blog post published: " + state.postId());
|
||||||
|
cmd.replyTo.tell(Done.getInstance());
|
||||||
|
}))
|
||||||
|
.matchCommand(GetPost.class, (state, cmd) -> {
|
||||||
|
cmd.replyTo.tell(state.postContent);
|
||||||
|
return Effect().none();
|
||||||
|
})
|
||||||
|
.matchCommand(AddPost.class, (state, cmd) -> Effect().unhandled())
|
||||||
|
.matchCommand(PassivatePost.class, cmd -> Effect().stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlogBehavior(PersistenceId persistenceId) {
|
||||||
|
super(persistenceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlogState emptyState() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandHandler<BlogCommand, BlogEvent, BlogState> commandHandler() {
|
||||||
|
return initialCommandHandler().orElse(postCommandHandler()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EventHandler<BlogState, BlogEvent> eventHandler() {
|
||||||
|
return eventHandlerBuilder()
|
||||||
|
.matchEvent(PostAdded.class, event ->
|
||||||
|
new BlogState(event.content, false))
|
||||||
|
.matchEvent(BodyChanged.class, (state, chg) ->
|
||||||
|
state.withContent(
|
||||||
|
new PostContent(state.postId(), state.postContent.title, chg.newBody)))
|
||||||
|
.matchEvent(Published.class, (state, event) ->
|
||||||
|
new BlogState(state.postContent, true))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -129,7 +129,7 @@ public class OptionalBlogState {
|
||||||
return Effect().persist(event)
|
return Effect().persist(event)
|
||||||
.andThen(() -> cmd.replyTo.tell(new AddPostDone(cmd.content.postId)));
|
.andThen(() -> cmd.replyTo.tell(new AddPostDone(cmd.content.postId)));
|
||||||
})
|
})
|
||||||
.matchCommand(PassivatePost.class, (state, cmd) -> Effect().stop());
|
.matchCommand(PassivatePost.class, cmd -> Effect().stop());
|
||||||
}
|
}
|
||||||
|
|
||||||
private CommandHandlerBuilder<BlogCommand, BlogEvent, Optional<BlogState>, Optional<BlogState>> postCommandHandler() {
|
private CommandHandlerBuilder<BlogCommand, BlogEvent, Optional<BlogState>, Optional<BlogState>> postCommandHandler() {
|
||||||
|
|
@ -148,7 +148,7 @@ public class OptionalBlogState {
|
||||||
return Effect().none();
|
return Effect().none();
|
||||||
})
|
})
|
||||||
.matchCommand(AddPost.class, (state, cmd) -> Effect().unhandled())
|
.matchCommand(AddPost.class, (state, cmd) -> Effect().unhandled())
|
||||||
.matchCommand(PassivatePost.class, (state, cmd) -> Effect().stop());
|
.matchCommand(PassivatePost.class, cmd -> Effect().stop());
|
||||||
}
|
}
|
||||||
|
|
||||||
public BlogBehavior(PersistenceId persistenceId) {
|
public BlogBehavior(PersistenceId persistenceId) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.persistence.typed.scaladsl
|
||||||
|
|
||||||
|
import akka.actor.testkit.typed.TestKitSettings
|
||||||
|
import akka.actor.testkit.typed.scaladsl._
|
||||||
|
import akka.actor.typed.ActorRef
|
||||||
|
import akka.actor.typed.Behavior
|
||||||
|
import akka.persistence.typed.PersistenceId
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import org.scalatest.WordSpecLike
|
||||||
|
|
||||||
|
object NullEmptyStateSpec {
|
||||||
|
|
||||||
|
private val conf = ConfigFactory.parseString(
|
||||||
|
s"""
|
||||||
|
akka.persistence.journal.plugin = "akka.persistence.journal.inmem"
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
class NullEmptyStateSpec extends ScalaTestWithActorTestKit(NullEmptyStateSpec.conf) with WordSpecLike {
|
||||||
|
|
||||||
|
implicit val testSettings = TestKitSettings(system)
|
||||||
|
|
||||||
|
def primitiveState(persistenceId: PersistenceId, probe: ActorRef[String]): Behavior[String] =
|
||||||
|
PersistentBehavior[String, String, String](
|
||||||
|
persistenceId,
|
||||||
|
emptyState = null,
|
||||||
|
commandHandler = (_, command) ⇒ {
|
||||||
|
if (command == "stop")
|
||||||
|
Effect.stop
|
||||||
|
else
|
||||||
|
Effect.persist(command)
|
||||||
|
},
|
||||||
|
eventHandler = (state, event) ⇒ {
|
||||||
|
probe.tell("eventHandler:" + state + ":" + event)
|
||||||
|
if (state == null) event else state + event
|
||||||
|
}
|
||||||
|
).onRecoveryCompleted { s ⇒
|
||||||
|
probe.tell("onRecoveryCompleted:" + s)
|
||||||
|
}
|
||||||
|
|
||||||
|
"A typed persistent actor with primitive state" must {
|
||||||
|
"persist events and update state" in {
|
||||||
|
val probe = TestProbe[String]()
|
||||||
|
val b = primitiveState(PersistenceId("a"), probe.ref)
|
||||||
|
val ref1 = spawn(b)
|
||||||
|
probe.expectMessage("onRecoveryCompleted:null")
|
||||||
|
ref1 ! "one"
|
||||||
|
probe.expectMessage("eventHandler:null:one")
|
||||||
|
ref1 ! "two"
|
||||||
|
probe.expectMessage("eventHandler:one:two")
|
||||||
|
|
||||||
|
ref1 ! "stop"
|
||||||
|
val ref2 = testKit.spawn(b)
|
||||||
|
// eventHandler from reply
|
||||||
|
probe.expectMessage("eventHandler:null:one")
|
||||||
|
probe.expectMessage("eventHandler:one:two")
|
||||||
|
probe.expectMessage("onRecoveryCompleted:onetwo")
|
||||||
|
ref2 ! "three"
|
||||||
|
probe.expectMessage("eventHandler:onetwo:three")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.persistence.typed.scaladsl
|
||||||
|
|
||||||
|
import akka.actor.testkit.typed.TestKitSettings
|
||||||
|
import akka.actor.testkit.typed.scaladsl._
|
||||||
|
import akka.actor.typed.ActorRef
|
||||||
|
import akka.actor.typed.Behavior
|
||||||
|
import akka.persistence.typed.PersistenceId
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import org.scalatest.WordSpecLike
|
||||||
|
|
||||||
|
object PrimitiveStateSpec {
|
||||||
|
|
||||||
|
private val conf = ConfigFactory.parseString(
|
||||||
|
s"""
|
||||||
|
akka.persistence.journal.plugin = "akka.persistence.journal.inmem"
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrimitiveStateSpec extends ScalaTestWithActorTestKit(PrimitiveStateSpec.conf) with WordSpecLike {
|
||||||
|
|
||||||
|
implicit val testSettings = TestKitSettings(system)
|
||||||
|
|
||||||
|
def primitiveState(persistenceId: PersistenceId, probe: ActorRef[String]): Behavior[Int] =
|
||||||
|
PersistentBehavior[Int, Int, Int](
|
||||||
|
persistenceId,
|
||||||
|
emptyState = 0,
|
||||||
|
commandHandler = (_, command) ⇒ {
|
||||||
|
if (command < 0)
|
||||||
|
Effect.stop
|
||||||
|
else
|
||||||
|
Effect.persist(command)
|
||||||
|
},
|
||||||
|
eventHandler = (state, event) ⇒ {
|
||||||
|
probe.tell("eventHandler:" + state + ":" + event)
|
||||||
|
state + event
|
||||||
|
}
|
||||||
|
).onRecoveryCompleted { n ⇒
|
||||||
|
probe.tell("onRecoveryCompleted:" + n)
|
||||||
|
}
|
||||||
|
|
||||||
|
"A typed persistent actor with primitive state" must {
|
||||||
|
"persist events and update state" in {
|
||||||
|
val probe = TestProbe[String]()
|
||||||
|
val b = primitiveState(PersistenceId("a"), probe.ref)
|
||||||
|
val ref1 = spawn(b)
|
||||||
|
probe.expectMessage("onRecoveryCompleted:0")
|
||||||
|
ref1 ! 1
|
||||||
|
probe.expectMessage("eventHandler:0:1")
|
||||||
|
ref1 ! 2
|
||||||
|
probe.expectMessage("eventHandler:1:2")
|
||||||
|
|
||||||
|
ref1 ! -1
|
||||||
|
val ref2 = testKit.spawn(b)
|
||||||
|
// eventHandler from reply
|
||||||
|
probe.expectMessage("eventHandler:0:1")
|
||||||
|
probe.expectMessage("eventHandler:1:2")
|
||||||
|
probe.expectMessage("onRecoveryCompleted:3")
|
||||||
|
ref2 ! 3
|
||||||
|
probe.expectMessage("eventHandler:3:3")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue