Rename sbt akka modules

Co-authored-by: Sean Glover <sean@seanglover.com>
This commit is contained in:
Matthew de Detrich 2023-01-05 11:10:50 +01:00 committed by GitHub
parent b92b749946
commit 24c03cde19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2930 changed files with 1466 additions and 1462 deletions

View file

@ -0,0 +1,75 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package jdocs.org.apache.pekko.persistence.typed;
import org.apache.pekko.actor.typed.Behavior;
import org.apache.pekko.persistence.typed.ReplicaId;
import org.apache.pekko.persistence.typed.ReplicationId;
import org.apache.pekko.persistence.typed.javadsl.*;
import java.util.*;
// #factory
public class MyReplicatedBehavior
extends ReplicatedEventSourcedBehavior<
MyReplicatedBehavior.Command, MyReplicatedBehavior.Event, MyReplicatedBehavior.State> {
// #factory
interface Command {}
interface State {}
interface Event {}
// #replicas
public static final ReplicaId DCA = new ReplicaId("DCA");
public static final ReplicaId DCB = new ReplicaId("DCB");
public static final Set<ReplicaId> ALL_REPLICAS =
Collections.unmodifiableSet(new HashSet<>(Arrays.asList(DCA, DCB)));
// #replicas
// #factory-shared
public static Behavior<Command> create(
String entityId, ReplicaId replicaId, String queryPluginId) {
return ReplicatedEventSourcing.commonJournalConfig(
new ReplicationId("MyReplicatedEntity", entityId, replicaId),
ALL_REPLICAS,
queryPluginId,
MyReplicatedBehavior::new);
}
// #factory-shared
// #factory
public static Behavior<Command> create(String entityId, ReplicaId replicaId) {
Map<ReplicaId, String> allReplicasAndQueryPlugins = new HashMap<>();
allReplicasAndQueryPlugins.put(DCA, "journalForDCA");
allReplicasAndQueryPlugins.put(DCB, "journalForDCB");
return ReplicatedEventSourcing.perReplicaJournalConfig(
new ReplicationId("MyReplicatedEntity", entityId, replicaId),
allReplicasAndQueryPlugins,
MyReplicatedBehavior::new);
}
private MyReplicatedBehavior(ReplicationContext replicationContext) {
super(replicationContext);
}
// #factory
@Override
public State emptyState() {
throw new UnsupportedOperationException("dummy for example");
}
@Override
public CommandHandler<Command, Event, State> commandHandler() {
throw new UnsupportedOperationException("dummy for example");
}
@Override
public EventHandler<State, Event> eventHandler() {
throw new UnsupportedOperationException("dummy for example");
}
}

View file

@ -0,0 +1,446 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package jdocs.org.apache.pekko.persistence.typed;
import org.apache.pekko.actor.testkit.typed.javadsl.LogCapturing;
import org.apache.pekko.actor.testkit.typed.javadsl.TestKitJunitResource;
import org.apache.pekko.actor.testkit.typed.javadsl.TestProbe;
import org.apache.pekko.actor.typed.ActorRef;
import org.apache.pekko.actor.typed.Behavior;
import org.apache.pekko.actor.typed.javadsl.ActorContext;
import org.apache.pekko.actor.typed.javadsl.Behaviors;
import org.apache.pekko.actor.typed.javadsl.TimerScheduler;
import org.apache.pekko.persistence.testkit.PersistenceTestKitPlugin;
import org.apache.pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal;
import org.apache.pekko.persistence.typed.RecoveryCompleted;
import org.apache.pekko.persistence.typed.ReplicaId;
import org.apache.pekko.persistence.typed.ReplicationId;
import org.apache.pekko.persistence.typed.javadsl.CommandHandler;
import org.apache.pekko.persistence.typed.javadsl.CommandHandlerBuilder;
import org.apache.pekko.persistence.typed.javadsl.EventHandler;
import org.apache.pekko.persistence.typed.javadsl.ReplicatedEventSourcedBehavior;
import org.apache.pekko.persistence.typed.javadsl.ReplicatedEventSourcing;
import org.apache.pekko.persistence.typed.javadsl.ReplicationContext;
import org.apache.pekko.persistence.typed.javadsl.SignalHandler;
import org.apache.pekko.serialization.jackson.CborSerializable;
import com.fasterxml.jackson.annotation.JsonCreator;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.scalatestplus.junit.JUnitSuite;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import static jdocs.org.apache.pekko.persistence.typed.AuctionEntity.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class ReplicatedAuctionExampleTest extends JUnitSuite {
@ClassRule
public static final TestKitJunitResource testKit =
new TestKitJunitResource(PersistenceTestKitPlugin.getInstance().config());
@Rule public final LogCapturing logCapturing = new LogCapturing();
@Test
public void auctionExample() {
String auctionName = "old-skis";
Bid initialBid = new Bid("chbatey", 12, Instant.now(), R1);
Instant closeAt = Instant.now().plusSeconds(10);
ActorRef<Command> replicaA =
testKit.spawn(AuctionEntity.create(R1, auctionName, initialBid, closeAt, true));
ActorRef<Command> replicaB =
testKit.spawn(AuctionEntity.create(R2, auctionName, initialBid, closeAt, false));
replicaA.tell(new OfferBid("me", 100));
replicaA.tell(new OfferBid("me", 99));
replicaA.tell(new OfferBid("me", 202));
TestProbe<Bid> replyProbe = testKit.createTestProbe();
replyProbe.awaitAssert(
() -> {
replicaA.tell(new GetHighestBid(replyProbe.ref()));
Bid bid = replyProbe.expectMessageClass(Bid.class);
assertEquals(202, bid.offer);
return bid;
});
replicaA.tell(Finish.INSTANCE);
TestProbe<Boolean> finishProbe = testKit.createTestProbe();
finishProbe.awaitAssert(
() -> {
replicaA.tell(new IsClosed(finishProbe.ref()));
return finishProbe.expectMessage(true);
});
finishProbe.awaitAssert(
() -> {
replicaB.tell(new IsClosed(finishProbe.ref()));
return finishProbe.expectMessage(true);
});
}
}
// #setup
class AuctionEntity
extends ReplicatedEventSourcedBehavior<
AuctionEntity.Command, AuctionEntity.Event, AuctionEntity.AuctionState> {
public static ReplicaId R1 = new ReplicaId("R1");
public static ReplicaId R2 = new ReplicaId("R2");
public static Set<ReplicaId> ALL_REPLICAS = new HashSet<>(Arrays.asList(R1, R2));
private final ActorContext<Command> context;
private final TimerScheduler<Command> timers;
private final Bid initialBid;
private final Instant closingAt;
private final boolean responsibleForClosing;
// #setup
// #commands
public static final class Bid {
public final String bidder;
public final int offer;
public final Instant timestamp;
public final ReplicaId originReplica;
public Bid(String bidder, int offer, Instant timestamp, ReplicaId originReplica) {
this.bidder = bidder;
this.offer = offer;
this.timestamp = timestamp;
this.originReplica = originReplica;
}
}
interface Command extends CborSerializable {}
public enum Finish implements Command {
INSTANCE
}
public static final class OfferBid implements Command {
public final String bidder;
public final int offer;
public OfferBid(String bidder, int offer) {
this.bidder = bidder;
this.offer = offer;
}
}
public static final class GetHighestBid implements Command {
public final ActorRef<Bid> replyTo;
public GetHighestBid(ActorRef<Bid> replyTo) {
this.replyTo = replyTo;
}
}
public static final class IsClosed implements Command {
public final ActorRef<Boolean> replyTo;
public IsClosed(ActorRef<Boolean> replyTo) {
this.replyTo = replyTo;
}
}
private enum Close implements Command {
INSTANCE
}
// #commands
// #events
interface Event extends CborSerializable {}
public static final class BidRegistered implements Event {
public final Bid bid;
@JsonCreator
public BidRegistered(Bid bid) {
this.bid = bid;
}
}
public static final class AuctionFinished implements Event {
public final ReplicaId atReplica;
@JsonCreator
public AuctionFinished(ReplicaId atReplica) {
this.atReplica = atReplica;
}
}
public static final class WinnerDecided implements Event {
public final ReplicaId atReplica;
public final Bid winningBid;
public final int amount;
public WinnerDecided(ReplicaId atReplica, Bid winningBid, int amount) {
this.atReplica = atReplica;
this.winningBid = winningBid;
this.amount = amount;
}
}
// #events
// #state
static class AuctionState implements CborSerializable {
final boolean stillRunning;
final Bid highestBid;
final int highestCounterOffer;
final Set<String> finishedAtDc;
AuctionState(
boolean stillRunning, Bid highestBid, int highestCounterOffer, Set<String> finishedAtDc) {
this.stillRunning = stillRunning;
this.highestBid = highestBid;
this.highestCounterOffer = highestCounterOffer;
this.finishedAtDc = finishedAtDc;
}
AuctionState withNewHighestBid(Bid bid) {
assertTrue(stillRunning);
assertTrue(isHigherBid(bid, highestBid));
return new AuctionState(
stillRunning, bid, highestBid.offer, finishedAtDc); // keep last highest bid around
}
AuctionState withTooLowBid(Bid bid) {
assertTrue(stillRunning);
assertTrue(isHigherBid(highestBid, bid));
return new AuctionState(
stillRunning, highestBid, Math.max(highestCounterOffer, bid.offer), finishedAtDc);
}
static Boolean isHigherBid(Bid first, Bid second) {
return first.offer > second.offer
|| (first.offer == second.offer && first.timestamp.isBefore(second.timestamp))
|| // if equal, first one wins
// If timestamps are equal, choose by dc where the offer was submitted
// In real auctions, this last comparison should be deterministic but unpredictable, so
// that submitting to a
// particular DC would not be an advantage.
(first.offer == second.offer
&& first.timestamp.equals(second.timestamp)
&& first.originReplica.id().compareTo(second.originReplica.id()) < 0);
}
AuctionState addFinishedAtReplica(String replica) {
Set<String> s = new HashSet<>(finishedAtDc);
s.add(replica);
return new AuctionState(
false, highestBid, highestCounterOffer, Collections.unmodifiableSet(s));
}
public AuctionState close() {
return new AuctionState(false, highestBid, highestCounterOffer, Collections.emptySet());
}
public boolean isClosed() {
return !stillRunning && finishedAtDc.isEmpty();
}
}
// #state
// #setup
public static Behavior<Command> create(
ReplicaId replica,
String name,
Bid initialBid,
Instant closingAt,
boolean responsibleForClosing) {
return Behaviors.setup(
ctx ->
Behaviors.withTimers(
timers ->
ReplicatedEventSourcing.commonJournalConfig(
new ReplicationId("Auction", name, replica),
ALL_REPLICAS,
PersistenceTestKitReadJournal.Identifier(),
replicationCtx ->
new AuctionEntity(
ctx,
replicationCtx,
timers,
initialBid,
closingAt,
responsibleForClosing))));
}
private AuctionEntity(
ActorContext<Command> context,
ReplicationContext replicationContext,
TimerScheduler<Command> timers,
Bid initialBid,
Instant closingAt,
boolean responsibleForClosing) {
super(replicationContext);
this.context = context;
this.timers = timers;
this.initialBid = initialBid;
this.closingAt = closingAt;
this.responsibleForClosing = responsibleForClosing;
}
@Override
public AuctionState emptyState() {
return new AuctionState(true, initialBid, initialBid.offer, Collections.emptySet());
}
@Override
public SignalHandler<AuctionState> signalHandler() {
return newSignalHandlerBuilder()
.onSignal(RecoveryCompleted.instance(), this::onRecoveryCompleted)
.build();
}
private void onRecoveryCompleted(AuctionState state) {
if (shouldClose(state)) {
context.getSelf().tell(Close.INSTANCE);
}
long millisUntilClosing =
closingAt.toEpochMilli() - getReplicationContext().currentTimeMillis();
timers.startSingleTimer(Finish.INSTANCE, Duration.ofMillis(millisUntilClosing));
}
// #setup
// #command-handler
@Override
public CommandHandler<Command, Event, AuctionState> commandHandler() {
CommandHandlerBuilder<Command, Event, AuctionState> builder = newCommandHandlerBuilder();
// running
builder
.forState(state -> state.stillRunning)
.onCommand(
OfferBid.class,
(state, bid) ->
Effect()
.persist(
new BidRegistered(
new Bid(
bid.bidder,
bid.offer,
Instant.ofEpochMilli(
this.getReplicationContext().currentTimeMillis()),
this.getReplicationContext().replicaId()))))
.onCommand(
GetHighestBid.class,
(state, get) -> {
get.replyTo.tell(state.highestBid);
return Effect().none();
})
.onCommand(
Finish.class,
(state, finish) ->
Effect().persist(new AuctionFinished(getReplicationContext().replicaId())))
.onCommand(Close.class, (state, close) -> Effect().unhandled())
.onCommand(
IsClosed.class,
(state, get) -> {
get.replyTo.tell(false);
return Effect().none();
});
// finished
builder
.forAnyState()
.onCommand(OfferBid.class, (state, bid) -> Effect().unhandled())
.onCommand(
GetHighestBid.class,
(state, get) -> {
get.replyTo.tell(state.highestBid);
return Effect().none();
})
.onCommand(
Finish.class,
(state, finish) ->
Effect().persist(new AuctionFinished(getReplicationContext().replicaId())))
.onCommand(
Close.class,
(state, close) ->
Effect()
.persist(
new WinnerDecided(
getReplicationContext().replicaId(),
state.highestBid,
state.highestCounterOffer)))
.onCommand(
IsClosed.class,
(state, get) -> {
get.replyTo.tell(state.isClosed());
return Effect().none();
});
return builder.build();
}
// #command-handler
// #event-handler
@Override
public EventHandler<AuctionState, Event> eventHandler() {
return newEventHandlerBuilder()
.forAnyState()
.onEvent(
BidRegistered.class,
(state, event) -> {
if (AuctionState.isHigherBid(event.bid, state.highestBid)) {
return state.withNewHighestBid(event.bid);
} else {
return state.withTooLowBid(event.bid);
}
})
.onEvent(
AuctionFinished.class,
(state, event) -> {
AuctionState newState = state.addFinishedAtReplica(event.atReplica.id());
if (state.isClosed()) return state; // already closed
else if (!getReplicationContext().recoveryRunning()) {
eventTriggers(event, newState);
}
return newState;
})
.onEvent(WinnerDecided.class, (state, event) -> state.close())
.build();
}
// #event-handler
// #event-triggers
private void eventTriggers(AuctionFinished event, AuctionState newState) {
if (newState.finishedAtDc.contains(getReplicationContext().replicaId().id())) {
if (shouldClose(newState)) {
context.getSelf().tell(Close.INSTANCE);
}
} else {
context.getSelf().tell(Finish.INSTANCE);
}
}
private boolean shouldClose(AuctionState state) {
return responsibleForClosing
&& !state.isClosed()
&& getReplicationContext().getAllReplicas().stream()
.map(ReplicaId::id)
.collect(Collectors.toSet())
.equals(state.finishedAtDc);
}
// #event-triggers
// #setup
}
// #setup

View file

@ -0,0 +1,291 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package jdocs.org.apache.pekko.persistence.typed;
import org.apache.pekko.Done;
import org.apache.pekko.actor.typed.ActorRef;
import org.apache.pekko.actor.typed.Behavior;
import org.apache.pekko.actor.typed.javadsl.ActorContext;
import org.apache.pekko.actor.typed.javadsl.Behaviors;
import org.apache.pekko.persistence.testkit.query.javadsl.PersistenceTestKitReadJournal;
import org.apache.pekko.persistence.typed.ReplicaId;
import org.apache.pekko.persistence.typed.ReplicationId;
import org.apache.pekko.persistence.typed.crdt.LwwTime;
import org.apache.pekko.persistence.typed.javadsl.CommandHandler;
import org.apache.pekko.persistence.typed.javadsl.Effect;
import org.apache.pekko.persistence.typed.javadsl.EventHandler;
import org.apache.pekko.persistence.typed.javadsl.ReplicatedEventSourcedBehavior;
import org.apache.pekko.persistence.typed.javadsl.ReplicatedEventSourcing;
import org.apache.pekko.persistence.typed.javadsl.ReplicationContext;
import java.util.Optional;
import java.util.Set;
interface ReplicatedBlogExample {
public final class BlogEntity
extends ReplicatedEventSourcedBehavior<
BlogEntity.Command, BlogEntity.Event, BlogEntity.BlogState> {
private final ActorContext<Command> context;
interface Command {
String getPostId();
}
static final class AddPost implements Command {
final String postId;
final PostContent content;
final ActorRef<AddPostDone> replyTo;
public AddPost(String postId, PostContent content, ActorRef<AddPostDone> replyTo) {
this.postId = postId;
this.content = content;
this.replyTo = replyTo;
}
public String getPostId() {
return postId;
}
}
static final class AddPostDone {
final String postId;
AddPostDone(String postId) {
this.postId = postId;
}
public String getPostId() {
return postId;
}
}
static final class GetPost implements Command {
final String postId;
final ActorRef<PostContent> replyTo;
public GetPost(String postId, ActorRef<PostContent> replyTo) {
this.postId = postId;
this.replyTo = replyTo;
}
public String getPostId() {
return postId;
}
}
static final class ChangeBody implements Command {
final String postId;
final PostContent newContent;
final ActorRef<Done> replyTo;
public ChangeBody(String postId, PostContent newContent, ActorRef<Done> replyTo) {
this.postId = postId;
this.newContent = newContent;
this.replyTo = replyTo;
}
public String getPostId() {
return postId;
}
}
static final class Publish implements Command {
final String postId;
final ActorRef<Done> replyTo;
public Publish(String postId, ActorRef<Done> replyTo) {
this.postId = postId;
this.replyTo = replyTo;
}
public String getPostId() {
return postId;
}
}
interface Event {}
static final class PostAdded implements Event {
final String postId;
final PostContent content;
final LwwTime timestamp;
public PostAdded(String postId, PostContent content, LwwTime timestamp) {
this.postId = postId;
this.content = content;
this.timestamp = timestamp;
}
}
static final class BodyChanged implements Event {
final String postId;
final PostContent content;
final LwwTime timestamp;
public BodyChanged(String postId, PostContent content, LwwTime timestamp) {
this.postId = postId;
this.content = content;
this.timestamp = timestamp;
}
}
static final class Published implements Event {
final String postId;
public Published(String postId) {
this.postId = postId;
}
}
public static final class PostContent {
final String title;
final String body;
public PostContent(String title, String body) {
this.title = title;
this.body = body;
}
}
public static class BlogState {
public static final BlogState EMPTY =
new BlogState(Optional.empty(), new LwwTime(Long.MIN_VALUE, new ReplicaId("")), false);
final Optional<PostContent> content;
final LwwTime contentTimestamp;
final boolean published;
public BlogState(Optional<PostContent> content, LwwTime contentTimestamp, boolean published) {
this.content = content;
this.contentTimestamp = contentTimestamp;
this.published = published;
}
BlogState withContent(PostContent newContent, LwwTime timestamp) {
return new BlogState(Optional.of(newContent), timestamp, this.published);
}
BlogState publish() {
if (published) {
return this;
} else {
return new BlogState(content, contentTimestamp, true);
}
}
boolean isEmpty() {
return !content.isPresent();
}
}
public static Behavior<Command> create(
String entityId, ReplicaId replicaId, Set<ReplicaId> allReplicas) {
return Behaviors.setup(
context ->
ReplicatedEventSourcing.commonJournalConfig(
new ReplicationId("blog", entityId, replicaId),
allReplicas,
PersistenceTestKitReadJournal.Identifier(),
replicationContext -> new BlogEntity(context, replicationContext)));
}
private BlogEntity(ActorContext<Command> context, ReplicationContext replicationContext) {
super(replicationContext);
this.context = context;
}
@Override
public BlogState emptyState() {
return BlogState.EMPTY;
}
// #command-handler
@Override
public CommandHandler<Command, Event, BlogState> commandHandler() {
return newCommandHandlerBuilder()
.forAnyState()
.onCommand(AddPost.class, this::onAddPost)
.onCommand(ChangeBody.class, this::onChangeBody)
.onCommand(Publish.class, this::onPublish)
.onCommand(GetPost.class, this::onGetPost)
.build();
}
private Effect<Event, BlogState> onAddPost(BlogState state, AddPost command) {
PostAdded evt =
new PostAdded(
getReplicationContext().entityId(),
command.content,
state.contentTimestamp.increase(
getReplicationContext().currentTimeMillis(),
getReplicationContext().replicaId()));
return Effect()
.persist(evt)
.thenRun(() -> command.replyTo.tell(new AddPostDone(getReplicationContext().entityId())));
}
private Effect<Event, BlogState> onChangeBody(BlogState state, ChangeBody command) {
BodyChanged evt =
new BodyChanged(
getReplicationContext().entityId(),
command.newContent,
state.contentTimestamp.increase(
getReplicationContext().currentTimeMillis(),
getReplicationContext().replicaId()));
return Effect().persist(evt).thenRun(() -> command.replyTo.tell(Done.getInstance()));
}
private Effect<Event, BlogState> onPublish(BlogState state, Publish command) {
Published evt = new Published(getReplicationContext().entityId());
return Effect().persist(evt).thenRun(() -> command.replyTo.tell(Done.getInstance()));
}
private Effect<Event, BlogState> onGetPost(BlogState state, GetPost command) {
context.getLog().info("GetPost {}", state.content);
if (state.content.isPresent()) command.replyTo.tell(state.content.get());
return Effect().none();
}
// #command-handler
// #event-handler
@Override
public EventHandler<BlogState, Event> eventHandler() {
return newEventHandlerBuilder()
.forAnyState()
.onEvent(PostAdded.class, this::onPostAdded)
.onEvent(BodyChanged.class, this::onBodyChanged)
.onEvent(Published.class, this::onPublished)
.build();
}
private BlogState onPostAdded(BlogState state, PostAdded event) {
if (event.timestamp.isAfter(state.contentTimestamp)) {
BlogState s = state.withContent(event.content, event.timestamp);
context.getLog().info("Updating content. New content is {}", s);
return s;
} else {
context.getLog().info("Ignoring event as timestamp is older");
return state;
}
}
private BlogState onBodyChanged(BlogState state, BodyChanged event) {
if (event.timestamp.isAfter(state.contentTimestamp)) {
return state.withContent(event.content, event.timestamp);
} else {
return state;
}
}
private BlogState onPublished(BlogState state, Published event) {
return state.publish();
}
// #event-handler
}
}

View file

@ -0,0 +1,104 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package jdocs.org.apache.pekko.persistence.typed;
import org.apache.pekko.actor.typed.ActorRef;
import org.apache.pekko.actor.typed.Behavior;
import org.apache.pekko.persistence.testkit.query.javadsl.PersistenceTestKitReadJournal;
import org.apache.pekko.persistence.typed.ReplicaId;
import org.apache.pekko.persistence.typed.ReplicationId;
import org.apache.pekko.persistence.typed.crdt.ORSet;
import org.apache.pekko.persistence.typed.javadsl.CommandHandler;
import org.apache.pekko.persistence.typed.javadsl.EventHandler;
import org.apache.pekko.persistence.typed.javadsl.ReplicatedEventSourcedBehavior;
import org.apache.pekko.persistence.typed.javadsl.ReplicatedEventSourcing;
import org.apache.pekko.persistence.typed.javadsl.ReplicationContext;
import java.util.Collections;
import java.util.Set;
interface ReplicatedMovieExample {
// #movie-entity
public final class MovieWatchList
extends ReplicatedEventSourcedBehavior<MovieWatchList.Command, ORSet.DeltaOp, ORSet<String>> {
interface Command {}
public static class AddMovie implements Command {
public final String movieId;
public AddMovie(String movieId) {
this.movieId = movieId;
}
}
public static class RemoveMovie implements Command {
public final String movieId;
public RemoveMovie(String movieId) {
this.movieId = movieId;
}
}
public static class GetMovieList implements Command {
public final ActorRef<MovieList> replyTo;
public GetMovieList(ActorRef<MovieList> replyTo) {
this.replyTo = replyTo;
}
}
public static class MovieList {
public final Set<String> movieIds;
public MovieList(Set<String> movieIds) {
this.movieIds = Collections.unmodifiableSet(movieIds);
}
}
public static Behavior<Command> create(
String entityId, ReplicaId replicaId, Set<ReplicaId> allReplicas) {
return ReplicatedEventSourcing.commonJournalConfig(
new ReplicationId("movies", entityId, replicaId),
allReplicas,
PersistenceTestKitReadJournal.Identifier(),
MovieWatchList::new);
}
private MovieWatchList(ReplicationContext replicationContext) {
super(replicationContext);
}
@Override
public ORSet<String> emptyState() {
return ORSet.empty(getReplicationContext().replicaId());
}
@Override
public CommandHandler<Command, ORSet.DeltaOp, ORSet<String>> commandHandler() {
return newCommandHandlerBuilder()
.forAnyState()
.onCommand(
AddMovie.class, (state, command) -> Effect().persist(state.add(command.movieId)))
.onCommand(
RemoveMovie.class,
(state, command) -> Effect().persist(state.remove(command.movieId)))
.onCommand(
GetMovieList.class,
(state, command) -> {
command.replyTo.tell(new MovieList(state.getElements()));
return Effect().none();
})
.build();
}
@Override
public EventHandler<ORSet<String>, ORSet.DeltaOp> eventHandler() {
return newEventHandlerBuilder().forAnyState().onAnyEvent(ORSet::applyOperation);
}
}
// #movie-entity
}

View file

@ -0,0 +1,158 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package jdocs.org.apache.pekko.persistence.typed;
import org.apache.pekko.actor.typed.ActorRef;
import org.apache.pekko.actor.typed.Behavior;
import org.apache.pekko.persistence.testkit.query.javadsl.PersistenceTestKitReadJournal;
import org.apache.pekko.persistence.typed.ReplicaId;
import org.apache.pekko.persistence.typed.ReplicationId;
import org.apache.pekko.persistence.typed.crdt.Counter;
import org.apache.pekko.persistence.typed.javadsl.CommandHandler;
import org.apache.pekko.persistence.typed.javadsl.Effect;
import org.apache.pekko.persistence.typed.javadsl.EventHandler;
import org.apache.pekko.persistence.typed.javadsl.ReplicatedEventSourcedBehavior;
import org.apache.pekko.persistence.typed.javadsl.ReplicatedEventSourcing;
import org.apache.pekko.persistence.typed.javadsl.ReplicationContext;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
interface ReplicatedShoppingCartExample {
// #shopping-cart
public final class ShoppingCart
extends ReplicatedEventSourcedBehavior<
ShoppingCart.Command, ShoppingCart.Event, ShoppingCart.State> {
public interface Event {}
public static final class ItemUpdated implements Event {
public final String productId;
public final Counter.Updated update;
public ItemUpdated(String productId, Counter.Updated update) {
this.productId = productId;
this.update = update;
}
}
public interface Command {}
public static final class AddItem implements Command {
public final String productId;
public final int count;
public AddItem(String productId, int count) {
this.productId = productId;
this.count = count;
}
}
public static final class RemoveItem implements Command {
public final String productId;
public final int count;
public RemoveItem(String productId, int count) {
this.productId = productId;
this.count = count;
}
}
public static class GetCartItems implements Command {
public final ActorRef<CartItems> replyTo;
public GetCartItems(ActorRef<CartItems> replyTo) {
this.replyTo = replyTo;
}
}
public static final class CartItems {
public final Map<String, Integer> items;
public CartItems(Map<String, Integer> items) {
this.items = items;
}
}
public static final class State {
public final Map<String, Counter> items = new HashMap<>();
}
public static Behavior<Command> create(
String entityId, ReplicaId replicaId, Set<ReplicaId> allReplicas) {
return ReplicatedEventSourcing.commonJournalConfig(
new ReplicationId("blog", entityId, replicaId),
allReplicas,
PersistenceTestKitReadJournal.Identifier(),
ShoppingCart::new);
}
private ShoppingCart(ReplicationContext replicationContext) {
super(replicationContext);
}
@Override
public State emptyState() {
return new State();
}
@Override
public CommandHandler<Command, Event, State> commandHandler() {
return newCommandHandlerBuilder()
.forAnyState()
.onCommand(AddItem.class, this::onAddItem)
.onCommand(RemoveItem.class, this::onRemoveItem)
.onCommand(GetCartItems.class, this::onGetCartItems)
.build();
}
private Effect<Event, State> onAddItem(State state, AddItem command) {
return Effect()
.persist(new ItemUpdated(command.productId, new Counter.Updated(command.count)));
}
private Effect<Event, State> onRemoveItem(State state, RemoveItem command) {
return Effect()
.persist(new ItemUpdated(command.productId, new Counter.Updated(-command.count)));
}
private Effect<Event, State> onGetCartItems(State state, GetCartItems command) {
command.replyTo.tell(new CartItems(filterEmptyAndNegative(state.items)));
return Effect().none();
}
private Map<String, Integer> filterEmptyAndNegative(Map<String, Counter> cart) {
Map<String, Integer> result = new HashMap<>();
for (Map.Entry<String, Counter> entry : cart.entrySet()) {
int count = entry.getValue().value().intValue();
if (count > 0) result.put(entry.getKey(), count);
}
return Collections.unmodifiableMap(result);
}
@Override
public EventHandler<State, Event> eventHandler() {
return newEventHandlerBuilder()
.forAnyState()
.onEvent(ItemUpdated.class, this::onItemUpdated)
.build();
}
private State onItemUpdated(State state, ItemUpdated event) {
final Counter counterForProduct;
if (state.items.containsKey(event.productId)) {
counterForProduct = state.items.get(event.productId);
} else {
counterForProduct = Counter.empty();
}
state.items.put(event.productId, counterForProduct.applyOperation(event.update));
return state;
}
}
// #shopping-cart
}

View file

@ -0,0 +1,86 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package jdocs.org.apache.pekko.persistence.typed;
import org.apache.pekko.actor.typed.Behavior;
import org.apache.pekko.persistence.testkit.query.javadsl.PersistenceTestKitReadJournal;
import org.apache.pekko.persistence.typed.ReplicaId;
import org.apache.pekko.persistence.typed.ReplicationId;
import org.apache.pekko.persistence.typed.javadsl.*;
import java.util.HashSet;
import java.util.Set;
public final class ReplicatedStringSet
extends ReplicatedEventSourcedBehavior<ReplicatedStringSet.Command, String, Set<String>> {
interface Command {}
public static final class AddString implements Command {
final String string;
public AddString(String string) {
this.string = string;
}
}
public static Behavior<Command> create(
String entityId, ReplicaId replicaId, Set<ReplicaId> allReplicas) {
return ReplicatedEventSourcing.commonJournalConfig(
new ReplicationId("StringSet", entityId, replicaId),
allReplicas,
PersistenceTestKitReadJournal.Identifier(),
ReplicatedStringSet::new);
}
private ReplicatedStringSet(ReplicationContext replicationContext) {
super(replicationContext);
}
@Override
public Set<String> emptyState() {
return new HashSet<>();
}
@Override
public CommandHandler<Command, String, Set<String>> commandHandler() {
return newCommandHandlerBuilder()
.forAnyState()
.onCommand(
AddString.class,
(state, cmd) -> {
if (!state.contains(cmd.string)) return Effect().persist(cmd.string);
else return Effect().none();
})
.build();
}
@Override
public EventHandler<Set<String>, String> eventHandler() {
return newEventHandlerBuilder()
.forAnyState()
.onAnyEvent(
(set, string) -> {
HashSet<String> newState = new HashSet<>(set);
newState.add(string);
return newState;
});
}
// #tagging
@Override
public Set<String> tagsFor(String event) {
// don't apply tags if event was replicated here, it already will appear in queries by tag
// as the origin replica would have tagged it already
if (getReplicationContext().replicaId() != getReplicationContext().origin()) {
return new HashSet<>();
} else {
Set<String> tags = new HashSet<>();
tags.add("strings");
if (event.length() > 10) tags.add("long-strings");
return tags;
}
}
// #tagging
}

View file

@ -0,0 +1,207 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed;
import org.apache.pekko.Done;
import org.apache.pekko.actor.testkit.typed.javadsl.LogCapturing;
import org.apache.pekko.actor.testkit.typed.javadsl.TestKitJunitResource;
import org.apache.pekko.actor.testkit.typed.javadsl.TestProbe;
import org.apache.pekko.actor.typed.ActorRef;
import org.apache.pekko.actor.typed.Behavior;
import org.apache.pekko.persistence.testkit.PersistenceTestKitPlugin;
import org.apache.pekko.persistence.testkit.query.javadsl.PersistenceTestKitReadJournal;
import org.apache.pekko.persistence.typed.javadsl.*;
import com.typesafe.config.ConfigFactory;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.scalatestplus.junit.JUnitSuite;
import java.util.*;
import static org.apache.pekko.Done.done;
import static org.junit.Assert.assertEquals;
public class ReplicatedEventSourcingTest extends JUnitSuite {
static final class TestBehavior
extends ReplicatedEventSourcedBehavior<TestBehavior.Command, String, Set<String>> {
interface Command {}
static final class GetState implements Command {
final ActorRef<State> replyTo;
public GetState(ActorRef<State> replyTo) {
this.replyTo = replyTo;
}
}
static final class StoreMe implements Command {
final String text;
final ActorRef<Done> replyTo;
public StoreMe(String text, ActorRef<Done> replyTo) {
this.text = text;
this.replyTo = replyTo;
}
}
static final class StoreUs implements Command {
final List<String> texts;
final ActorRef<Done> replyTo;
public StoreUs(List<String> texts, ActorRef<Done> replyTo) {
this.texts = texts;
this.replyTo = replyTo;
}
}
static final class GetReplica implements Command {
final ActorRef<ReplicaId> replyTo;
public GetReplica(ActorRef<ReplicaId> replyTo) {
this.replyTo = replyTo;
}
}
static final class State {
final Set<String> texts;
public State(Set<String> texts) {
this.texts = texts;
}
}
enum Stop implements Command {
INSTANCE
}
public static Behavior<Command> create(
String entityId, ReplicaId replicaId, Set<ReplicaId> allReplicas) {
return ReplicatedEventSourcing.commonJournalConfig(
new ReplicationId("ReplicatedEventSourcingTest", entityId, replicaId),
allReplicas,
PersistenceTestKitReadJournal.Identifier(),
TestBehavior::new);
}
private TestBehavior(ReplicationContext replicationContext) {
super(replicationContext);
}
@Override
public String journalPluginId() {
return PersistenceTestKitPlugin.PluginId();
}
@Override
public Set<String> emptyState() {
return Collections.emptySet();
}
@Override
public CommandHandler<Command, String, Set<String>> commandHandler() {
return newCommandHandlerBuilder()
.forAnyState()
.onCommand(
StoreMe.class,
(StoreMe cmd) -> Effect().persist(cmd.text).thenRun(__ -> cmd.replyTo.tell(done())))
.onCommand(
StoreUs.class,
(StoreUs cmd) -> Effect().persist(cmd.texts).thenRun(__ -> cmd.replyTo.tell(done())))
.onCommand(
GetState.class,
(GetState get) ->
Effect()
.none()
.thenRun(state -> get.replyTo.tell(new State(new HashSet<>(state)))))
.onCommand(
GetReplica.class,
(GetReplica cmd) ->
Effect()
.none()
.thenRun(() -> cmd.replyTo.tell(getReplicationContext().replicaId())))
.onCommand(Stop.class, __ -> Effect().stop())
.build();
}
@Override
public EventHandler<Set<String>, String> eventHandler() {
return newEventHandlerBuilder()
.forAnyState()
.onAnyEvent(
(state, text) -> {
// FIXME mutable - state I don't remember if we support or not so defensive copy for
// now
Set<String> newSet = new HashSet<>(state);
newSet.add(text);
return newSet;
});
}
}
@ClassRule
public static final TestKitJunitResource testKit =
new TestKitJunitResource(
ConfigFactory.parseString(
"pekko.loglevel = INFO\n"
+ "pekko.loggers = [\"org.apache.pekko.testkit.TestEventListener\"]")
.withFallback(PersistenceTestKitPlugin.getInstance().config()));
@Rule public final LogCapturing logCapturing = new LogCapturing();
// minimal test, full coverage over in ReplicatedEventSourcingSpec
@Test
public void replicatedEventSourcingReplicationTest() {
ReplicaId dcA = new ReplicaId("DC-A");
ReplicaId dcB = new ReplicaId("DC-B");
ReplicaId dcC = new ReplicaId("DC-C");
Set<ReplicaId> allReplicas = new HashSet<>(Arrays.asList(dcA, dcB, dcC));
ActorRef<TestBehavior.Command> replicaA =
testKit.spawn(TestBehavior.create("id1", dcA, allReplicas));
ActorRef<TestBehavior.Command> replicaB =
testKit.spawn(TestBehavior.create("id1", dcB, allReplicas));
ActorRef<TestBehavior.Command> replicaC =
testKit.spawn(TestBehavior.create("id1", dcC, allReplicas));
TestProbe<Object> probe = testKit.createTestProbe();
replicaA.tell(new TestBehavior.GetReplica(probe.ref().narrow()));
assertEquals("DC-A", probe.expectMessageClass(ReplicaId.class).id());
replicaA.tell(new TestBehavior.StoreMe("stored-to-a", probe.ref().narrow()));
replicaB.tell(new TestBehavior.StoreMe("stored-to-b", probe.ref().narrow()));
replicaC.tell(new TestBehavior.StoreMe("stored-to-c", probe.ref().narrow()));
probe.receiveSeveralMessages(3);
probe.awaitAssert(
() -> {
replicaA.tell(new TestBehavior.GetState(probe.ref().narrow()));
TestBehavior.State reply = probe.expectMessageClass(TestBehavior.State.class);
assertEquals(
new HashSet<>(Arrays.asList("stored-to-a", "stored-to-b", "stored-to-c")),
reply.texts);
return null;
});
probe.awaitAssert(
() -> {
replicaB.tell(new TestBehavior.GetState(probe.ref().narrow()));
TestBehavior.State reply = probe.expectMessageClass(TestBehavior.State.class);
assertEquals(
new HashSet<>(Arrays.asList("stored-to-a", "stored-to-b", "stored-to-c")),
reply.texts);
return null;
});
probe.awaitAssert(
() -> {
replicaC.tell(new TestBehavior.GetState(probe.ref().narrow()));
TestBehavior.State reply = probe.expectMessageClass(TestBehavior.State.class);
assertEquals(
new HashSet<>(Arrays.asList("stored-to-a", "stored-to-b", "stored-to-c")),
reply.texts);
return null;
});
}
}

View file

@ -0,0 +1,136 @@
/*
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.javadsl;
import org.apache.pekko.actor.testkit.typed.TestException;
import org.apache.pekko.actor.testkit.typed.javadsl.LogCapturing;
import org.apache.pekko.actor.testkit.typed.javadsl.TestKitJunitResource;
import org.apache.pekko.actor.testkit.typed.javadsl.TestProbe;
import org.apache.pekko.actor.typed.ActorRef;
import org.apache.pekko.actor.typed.Behavior;
import org.apache.pekko.actor.typed.SupervisorStrategy;
import org.apache.pekko.persistence.typed.PersistenceId;
import org.apache.pekko.persistence.typed.RecoveryCompleted;
import org.apache.pekko.persistence.typed.RecoveryFailed;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.scalatestplus.junit.JUnitSuite;
import java.time.Duration;
import static org.apache.pekko.persistence.typed.scaladsl.EventSourcedBehaviorFailureSpec.conf;
class FailingEventSourcedActor extends EventSourcedBehavior<String, String, String> {
private final ActorRef<String> probe;
private final ActorRef<Throwable> recoveryFailureProbe;
FailingEventSourcedActor(
PersistenceId persistenceId,
ActorRef<String> probe,
ActorRef<Throwable> recoveryFailureProbe) {
super(
persistenceId,
SupervisorStrategy.restartWithBackoff(Duration.ofMillis(1), Duration.ofMillis(5), 0.1));
this.probe = probe;
this.recoveryFailureProbe = recoveryFailureProbe;
}
@Override
public SignalHandler<String> signalHandler() {
return newSignalHandlerBuilder()
.onSignal(
RecoveryCompleted.instance(),
state -> {
probe.tell("starting");
})
.onSignal(
RecoveryFailed.class,
(state, signal) -> {
recoveryFailureProbe.tell(signal.getFailure());
})
.build();
}
@Override
public String emptyState() {
return "";
}
@Override
public CommandHandler<String, String, String> commandHandler() {
return (state, command) -> {
probe.tell("persisting");
return Effect().persist(command);
};
}
@Override
public EventHandler<String, String> eventHandler() {
return (state, event) -> {
probe.tell(event);
return state + event;
};
}
}
public class EventSourcedActorFailureTest extends JUnitSuite {
public static final Config config = conf().withFallback(ConfigFactory.load());
@ClassRule public static final TestKitJunitResource testKit = new TestKitJunitResource(config);
@Rule public final LogCapturing logCapturing = new LogCapturing();
public static Behavior<String> fail(
PersistenceId pid, ActorRef<String> probe, ActorRef<Throwable> recoveryFailureProbe) {
return new FailingEventSourcedActor(pid, probe, recoveryFailureProbe);
}
public static Behavior<String> fail(PersistenceId pid, ActorRef<String> probe) {
return fail(pid, probe, testKit.<Throwable>createTestProbe().ref());
}
@Test
public void notifyRecoveryFailure() {
TestProbe<String> probe = testKit.createTestProbe();
TestProbe<Throwable> recoveryFailureProbe = testKit.createTestProbe();
Behavior<String> p1 =
fail(
PersistenceId.ofUniqueId("fail-recovery-once"),
probe.ref(),
recoveryFailureProbe.ref());
testKit.spawn(p1);
recoveryFailureProbe.expectMessageClass(TestException.class);
}
@Test
public void persistEvents() throws Exception {
TestProbe<String> probe = testKit.createTestProbe();
Behavior<String> p1 = fail(PersistenceId.ofUniqueId("fail-first-2"), probe.ref());
ActorRef<String> c = testKit.spawn(p1);
probe.expectMessage("starting");
// fail
c.tell("one");
probe.expectMessage("persisting");
probe.expectMessage("one");
probe.expectMessage("starting");
// fail
c.tell("two");
probe.expectMessage("persisting");
probe.expectMessage("two");
probe.expectMessage("starting");
// work
c.tell("three");
probe.expectMessage("persisting");
probe.expectMessage("three");
// no starting as this one did not fail
probe.expectNoMessage();
}
}

View file

@ -0,0 +1,786 @@
/*
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.javadsl;
import org.apache.pekko.Done;
import org.apache.pekko.actor.testkit.typed.javadsl.LogCapturing;
import org.apache.pekko.actor.testkit.typed.javadsl.LoggingTestKit;
import org.apache.pekko.actor.testkit.typed.javadsl.TestKitJunitResource;
import org.apache.pekko.actor.testkit.typed.javadsl.TestProbe;
import org.apache.pekko.actor.typed.*;
import org.apache.pekko.actor.typed.javadsl.ActorContext;
import org.apache.pekko.actor.typed.javadsl.Adapter;
import org.apache.pekko.actor.typed.javadsl.Behaviors;
import org.apache.pekko.japi.Pair;
import org.apache.pekko.persistence.query.EventEnvelope;
import org.apache.pekko.persistence.query.NoOffset;
import org.apache.pekko.persistence.query.PersistenceQuery;
import org.apache.pekko.persistence.query.Sequence;
import org.apache.pekko.persistence.testkit.PersistenceTestKitPlugin;
import org.apache.pekko.persistence.testkit.PersistenceTestKitSnapshotPlugin;
import org.apache.pekko.persistence.testkit.query.javadsl.PersistenceTestKitReadJournal;
import org.apache.pekko.persistence.typed.*;
import org.apache.pekko.serialization.jackson.CborSerializable;
import org.apache.pekko.stream.javadsl.Sink;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.google.common.collect.Sets;
import com.typesafe.config.ConfigFactory;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.scalatestplus.junit.JUnitSuite;
import org.slf4j.event.Level;
import java.time.Duration;
import java.util.*;
import static org.apache.pekko.Done.done;
import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals;
public class EventSourcedBehaviorJavaDslTest extends JUnitSuite {
@ClassRule
public static final TestKitJunitResource testKit =
new TestKitJunitResource(
ConfigFactory.parseString(
"pekko.loglevel = INFO\n"
+ "pekko.loggers = [\"org.apache.pekko.testkit.TestEventListener\"]")
.withFallback(PersistenceTestKitPlugin.getInstance().config())
.withFallback(PersistenceTestKitSnapshotPlugin.config()));
@Rule public final LogCapturing logCapturing = new LogCapturing();
private PersistenceTestKitReadJournal queries =
PersistenceQuery.get(Adapter.toClassic(testKit.system()))
.getReadJournalFor(
PersistenceTestKitReadJournal.class, PersistenceTestKitReadJournal.Identifier());
interface Command extends CborSerializable {}
public enum Increment implements Command {
INSTANCE
}
public enum Increment100OnTimeout implements Command {
INSTANCE
}
public enum IncrementLater implements Command {
INSTANCE
}
public enum DelayFinished implements Command {
INSTANCE
}
public enum EmptyEventsListAndThenLog implements Command {
INSTANCE
}
public enum IncrementTwiceAndLog implements Command {
INSTANCE
}
public static class IncrementWithConfirmation implements Command {
public final ActorRef<Done> replyTo;
@JsonCreator
public IncrementWithConfirmation(ActorRef<Done> replyTo) {
this.replyTo = replyTo;
}
}
public enum StopThenLog implements Command {
INSTANCE
}
public enum Timeout implements Command {
INSTANCE
}
public static class GetValue implements Command {
public final ActorRef<State> replyTo;
@JsonCreator
public GetValue(ActorRef<State> replyTo) {
this.replyTo = replyTo;
}
}
// need the JsonTypeInfo because of the Wrapper
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({@JsonSubTypes.Type(value = Incremented.class, name = "incremented")})
interface Event extends CborSerializable {}
public static class Incremented implements Event {
final int delta;
@JsonCreator
public Incremented(int delta) {
this.delta = delta;
}
@Override
public String toString() {
return "Incremented{" + "delta=" + delta + '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Incremented that = (Incremented) o;
return delta == that.delta;
}
@Override
public int hashCode() {
return Objects.hash(delta);
}
}
public static class State implements CborSerializable {
final int value;
final List<Integer> history;
static final State EMPTY = new State(0, Collections.emptyList());
public State(int value, List<Integer> history) {
this.value = value;
this.history = history;
}
State incrementedBy(int delta) {
List<Integer> newHistory = new ArrayList<>(history);
newHistory.add(value);
return new State(value + delta, newHistory);
}
@Override
public String toString() {
return "State{" + "value=" + value + ", history=" + history + '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
State state = (State) o;
return value == state.value && Objects.equals(history, state.history);
}
@Override
public int hashCode() {
return Objects.hash(value, history);
}
}
public enum Tick {
INSTANCE
}
private Behavior<Command> counter(PersistenceId persistenceId) {
return Behaviors.setup(ctx -> new CounterBehavior(persistenceId, ctx));
}
@SuppressWarnings("unused")
private static class CounterBehavior extends EventSourcedBehavior<Command, Event, State> {
private final ActorContext<Command> ctx;
CounterBehavior(PersistenceId persistenceId, ActorContext<Command> ctx) {
super(
persistenceId,
SupervisorStrategy.restartWithBackoff(Duration.ofMillis(1), Duration.ofMillis(5), 0.1));
this.ctx = ctx;
}
@Override
public CommandHandler<Command, Event, State> commandHandler() {
return newCommandHandlerBuilder()
.forAnyState()
.onCommand(Increment.class, this::increment)
.onCommand(IncrementWithConfirmation.class, this::incrementWithConfirmation)
.onCommand(GetValue.class, this::getValue)
.onCommand(IncrementLater.class, this::incrementLater)
.onCommand(DelayFinished.class, this::delayFinished)
.onCommand(Increment100OnTimeout.class, this::increment100OnTimeout)
.onCommand(Timeout.class, this::timeout)
.onCommand(EmptyEventsListAndThenLog.class, this::emptyEventsListAndThenLog)
.onCommand(StopThenLog.class, this::stopThenLog)
.onCommand(IncrementTwiceAndLog.class, this::incrementTwiceAndLog)
.build();
}
private Effect<Event, State> increment(State state, Increment command) {
return Effect().persist(new Incremented(1));
}
private ReplyEffect<Event, State> incrementWithConfirmation(
State state, IncrementWithConfirmation command) {
return Effect().persist(new Incremented(1)).thenReply(command.replyTo, newState -> done());
}
private ReplyEffect<Event, State> getValue(State state, GetValue command) {
return Effect().reply(command.replyTo, state);
}
private Effect<Event, State> incrementLater(State state, IncrementLater command) {
ActorRef<Object> delay =
ctx.spawnAnonymous(
Behaviors.withTimers(
timers -> {
timers.startSingleTimer(Tick.INSTANCE, Tick.INSTANCE, Duration.ofMillis(10));
return Behaviors.receive((context, o) -> Behaviors.stopped());
}));
ctx.watchWith(delay, DelayFinished.INSTANCE);
return Effect().none();
}
private Effect<Event, State> delayFinished(State state, DelayFinished command) {
return Effect().persist(new Incremented(10));
}
private Effect<Event, State> increment100OnTimeout(State state, Increment100OnTimeout command) {
ctx.setReceiveTimeout(Duration.ofMillis(10), Timeout.INSTANCE);
return Effect().none();
}
private Effect<Event, State> timeout(State state, Timeout command) {
return Effect().persist(new Incremented(100));
}
private Effect<Event, State> emptyEventsListAndThenLog(
State state, EmptyEventsListAndThenLog command) {
return Effect().persist(Collections.emptyList()).thenRun(s -> log());
}
private Effect<Event, State> stopThenLog(State state, StopThenLog command) {
return Effect().stop().thenRun(s -> log());
}
private Effect<Event, State> incrementTwiceAndLog(State state, IncrementTwiceAndLog command) {
return Effect()
.persist(Arrays.asList(new Incremented(1), new Incremented(1)))
.thenRun(s -> log());
}
@Override
public EventHandler<State, Event> eventHandler() {
return newEventHandlerBuilder()
.forAnyState()
.onEvent(Incremented.class, this::applyIncremented)
.build();
}
@Override
public State emptyState() {
return State.EMPTY;
}
protected State applyIncremented(State state, Incremented event) {
// override to probe for events
return state.incrementedBy(event.delta);
}
protected void log() {
// override to probe for logs
}
}
@Test
public void persistEvents() {
ActorRef<Command> c = testKit.spawn(counter(PersistenceId.ofUniqueId("c1")));
TestProbe<State> probe = testKit.createTestProbe();
c.tell(Increment.INSTANCE);
c.tell(new GetValue(probe.ref()));
probe.expectMessage(new State(1, singletonList(0)));
}
@Test
public void replyStoredEvents() {
ActorRef<Command> c = testKit.spawn(counter(PersistenceId.ofUniqueId("c2")));
TestProbe<State> probe = testKit.createTestProbe();
c.tell(Increment.INSTANCE);
c.tell(Increment.INSTANCE);
c.tell(Increment.INSTANCE);
c.tell(new GetValue(probe.ref()));
probe.expectMessage(new State(3, Arrays.asList(0, 1, 2)));
ActorRef<Command> c2 = testKit.spawn(counter(PersistenceId.ofUniqueId("c2")));
c2.tell(new GetValue(probe.ref()));
probe.expectMessage(new State(3, Arrays.asList(0, 1, 2)));
c2.tell(Increment.INSTANCE);
c2.tell(new GetValue(probe.ref()));
probe.expectMessage(new State(4, Arrays.asList(0, 1, 2, 3)));
}
@Test
public void thenReplyEffect() {
ActorRef<Command> c = testKit.spawn(counter(PersistenceId.ofUniqueId("c1b")));
TestProbe<Done> probe = testKit.createTestProbe();
c.tell(new IncrementWithConfirmation(probe.ref()));
probe.expectMessage(Done.getInstance());
}
@Test
public void handleTerminatedSignal() {
TestProbe<Pair<State, Incremented>> eventHandlerProbe = testKit.createTestProbe();
Behavior<Command> counter =
Behaviors.setup(
ctx ->
new CounterBehavior(PersistenceId.ofUniqueId("c3"), ctx) {
@Override
protected State applyIncremented(State state, Incremented event) {
eventHandlerProbe.ref().tell(Pair.create(state, event));
return super.applyIncremented(state, event);
}
});
ActorRef<Command> c = testKit.spawn(counter);
c.tell(Increment.INSTANCE);
c.tell(IncrementLater.INSTANCE);
eventHandlerProbe.expectMessage(Pair.create(State.EMPTY, new Incremented(1)));
eventHandlerProbe.expectMessage(
Pair.create(new State(1, Collections.singletonList(0)), new Incremented(10)));
}
@Test
public void handleReceiveTimeout() {
TestProbe<Pair<State, Incremented>> eventHandlerProbe = testKit.createTestProbe();
Behavior<Command> counter =
Behaviors.setup(
ctx ->
new CounterBehavior(PersistenceId.ofUniqueId("c4"), ctx) {
@Override
protected State applyIncremented(State state, Incremented event) {
eventHandlerProbe.ref().tell(Pair.create(state, event));
return super.applyIncremented(state, event);
}
});
ActorRef<Command> c = testKit.spawn(counter);
c.tell(Increment100OnTimeout.INSTANCE);
eventHandlerProbe.expectMessage(Pair.create(State.EMPTY, new Incremented(100)));
}
@Test
public void chainableSideEffectsWithEvents() {
TestProbe<String> loggingProbe = testKit.createTestProbe();
Behavior<Command> counter =
Behaviors.setup(
ctx ->
new CounterBehavior(PersistenceId.ofUniqueId("c5"), ctx) {
@Override
protected void log() {
loggingProbe.ref().tell("logged");
}
});
ActorRef<Command> c = testKit.spawn(counter);
c.tell(EmptyEventsListAndThenLog.INSTANCE);
loggingProbe.expectMessage("logged");
}
@Test
public void workWhenWrappedInOtherBehavior() {
Behavior<Command> behavior =
Behaviors.supervise(counter(PersistenceId.ofUniqueId("c6")))
.onFailure(
SupervisorStrategy.restartWithBackoff(
Duration.ofSeconds(1), Duration.ofSeconds(10), 0.1));
ActorRef<Command> c = testKit.spawn(behavior);
TestProbe<State> probe = testKit.createTestProbe();
c.tell(Increment.INSTANCE);
c.tell(new GetValue(probe.ref()));
probe.expectMessage(new State(1, singletonList(0)));
}
@Test
public void snapshot() {
TestProbe<Optional<Throwable>> snapshotProbe = testKit.createTestProbe();
Behavior<Command> snapshoter =
Behaviors.setup(
ctx ->
new CounterBehavior(PersistenceId.ofUniqueId("snapshot"), ctx) {
@Override
public boolean shouldSnapshot(State state, Event event, long sequenceNr) {
return state.value % 2 == 0;
}
@Override
public SignalHandler<State> signalHandler() {
return newSignalHandlerBuilder()
.onSignal(
SnapshotCompleted.class,
(state, completed) -> {
snapshotProbe.ref().tell(Optional.empty());
})
.onSignal(
SnapshotFailed.class,
(state, signal) -> {
snapshotProbe.ref().tell(Optional.of(signal.getFailure()));
})
.build();
}
});
ActorRef<Command> c = testKit.spawn(snapshoter);
c.tell(Increment.INSTANCE);
c.tell(Increment.INSTANCE);
snapshotProbe.expectMessage(Optional.empty());
c.tell(Increment.INSTANCE);
TestProbe<State> stateProbe = testKit.createTestProbe();
c.tell(new GetValue(stateProbe.ref()));
stateProbe.expectMessage(new State(3, Arrays.asList(0, 1, 2)));
TestProbe<Pair<State, Incremented>> eventHandlerProbe = testKit.createTestProbe();
Behavior<Command> recovered =
Behaviors.setup(
ctx ->
new CounterBehavior(PersistenceId.ofUniqueId("snapshot"), ctx) {
@Override
protected State applyIncremented(State state, Incremented event) {
eventHandlerProbe.ref().tell(Pair.create(state, event));
return super.applyIncremented(state, event);
}
});
ActorRef<Command> c2 = testKit.spawn(recovered);
// First 2 are snapshot
eventHandlerProbe.expectMessage(
Pair.create(new State(2, Arrays.asList(0, 1)), new Incremented(1)));
c2.tell(new GetValue(stateProbe.ref()));
stateProbe.expectMessage(new State(3, Arrays.asList(0, 1, 2)));
}
@Test
public void stopThenLog() {
TestProbe<State> probe = testKit.createTestProbe();
ActorRef<Command> c = testKit.spawn(counter(PersistenceId.ofUniqueId("c12")));
c.tell(StopThenLog.INSTANCE);
probe.expectTerminated(c);
}
@Test
public void postStop() {
TestProbe<String> probe = testKit.createTestProbe();
Behavior<Command> counter =
Behaviors.setup(
ctx ->
new CounterBehavior(PersistenceId.ofUniqueId("c5"), ctx) {
@Override
public SignalHandler<State> signalHandler() {
return newSignalHandlerBuilder()
.onSignal(
PostStop.instance(),
state -> {
probe.ref().tell("stopped");
})
.build();
}
});
ActorRef<Command> c = testKit.spawn(counter);
c.tell(StopThenLog.INSTANCE);
probe.expectMessage("stopped");
}
@Test
public void tapPersistentActor() {
TestProbe<Object> interceptProbe = testKit.createTestProbe();
TestProbe<Signal> signalProbe = testKit.createTestProbe();
BehaviorInterceptor<Command, Command> tap =
new BehaviorInterceptor<Command, Command>(Command.class) {
@Override
public Behavior<Command> aroundReceive(
TypedActorContext<Command> ctx, Command msg, ReceiveTarget<Command> target) {
interceptProbe.ref().tell(msg);
return target.apply(ctx, msg);
}
@Override
public Behavior<Command> aroundSignal(
TypedActorContext<Command> ctx, Signal signal, SignalTarget<Command> target) {
signalProbe.ref().tell(signal);
return target.apply(ctx, signal);
}
};
ActorRef<Command> c =
testKit.spawn(Behaviors.intercept(() -> tap, counter(PersistenceId.ofUniqueId("tap1"))));
c.tell(Increment.INSTANCE);
interceptProbe.expectMessage(Increment.INSTANCE);
signalProbe.expectNoMessage();
}
@Test
public void tagEvent() throws Exception {
Behavior<Command> tagger =
Behaviors.setup(
ctx ->
new CounterBehavior(PersistenceId.ofUniqueId("tagging"), ctx) {
@Override
public Set<String> tagsFor(Event incremented) {
return Sets.newHashSet("tag1", "tag2");
}
});
ActorRef<Command> c = testKit.spawn(tagger);
c.tell(Increment.INSTANCE);
TestProbe<State> stateProbe = testKit.createTestProbe();
c.tell(new GetValue(stateProbe.ref()));
stateProbe.expectMessage(new State(1, Collections.singletonList(0)));
List<EventEnvelope> events =
queries
.currentEventsByTag("tag1", NoOffset.getInstance())
.runWith(Sink.seq(), testKit.system())
.toCompletableFuture()
.get();
assertEquals(1, events.size());
EventEnvelope eventEnvelope = events.get(0);
assertEquals(new Sequence(1), eventEnvelope.offset());
assertEquals("tagging", eventEnvelope.persistenceId());
assertEquals(new Incremented(1), eventEnvelope.event());
}
@Test
public void transformEvent() throws Exception {
Behavior<Command> transformer =
Behaviors.setup(
ctx ->
new CounterBehavior(PersistenceId.ofUniqueId("transform"), ctx) {
private final EventAdapter<Event, ?> adapter = new WrapperEventAdapter();
public EventAdapter<Event, ?> eventAdapter() {
return adapter;
}
});
ActorRef<Command> c = testKit.spawn(transformer);
c.tell(Increment.INSTANCE);
TestProbe<State> stateProbe = testKit.createTestProbe();
c.tell(new GetValue(stateProbe.ref()));
stateProbe.expectMessage(new State(1, Collections.singletonList(0)));
List<EventEnvelope> events =
queries
.currentEventsByPersistenceId("transform", 0, Long.MAX_VALUE)
.runWith(Sink.seq(), testKit.system())
.toCompletableFuture()
.get();
assertEquals(1, events.size());
EventEnvelope eventEnvelope = events.get(0);
assertEquals(new Sequence(1), eventEnvelope.offset());
assertEquals("transform", eventEnvelope.persistenceId());
assertEquals(new Wrapper(new Incremented(1)), eventEnvelope.event());
ActorRef<Command> c2 = testKit.spawn(transformer);
c2.tell(new GetValue(stateProbe.ref()));
stateProbe.expectMessage(new State(1, Collections.singletonList(0)));
}
// event-wrapper
public static class Wrapper implements CborSerializable {
private final Event event;
@JsonCreator
public Wrapper(Event event) {
this.event = event;
}
public Event getEvent() {
return event;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Wrapper wrapper = (Wrapper) o;
return event.equals(wrapper.event);
}
@Override
public int hashCode() {
return event.hashCode();
}
@Override
public String toString() {
return "Wrapper(" + event + ")";
}
}
class WrapperEventAdapter extends EventAdapter<Event, Wrapper> {
@Override
public Wrapper toJournal(Event event) {
return new Wrapper(event);
}
@Override
public String manifest(Event event) {
return "";
}
@Override
public EventSeq<Event> fromJournal(Wrapper wrapper, String manifest) {
return EventSeq.single(wrapper.getEvent());
}
}
// event-wrapper
static class IncorrectExpectedStateForThenRun
extends EventSourcedBehavior<String, String, Object> {
private final ActorRef<String> startedProbe;
public IncorrectExpectedStateForThenRun(
ActorRef<String> startedProbe, PersistenceId persistenceId) {
super(persistenceId);
this.startedProbe = startedProbe;
}
@Override
public Object emptyState() {
return 1;
}
@Override
public CommandHandler<String, String, Object> commandHandler() {
return newCommandHandlerBuilder()
.forAnyState()
.onCommand(
msg -> msg.equals("expect wrong type"),
(context) ->
Effect()
.none()
.thenRun(
(String wrongType) -> {
// wont happen
}))
.build();
}
@Override
public SignalHandler<Object> signalHandler() {
return newSignalHandlerBuilder()
.onSignal(
RecoveryCompleted.class,
(state, completed) -> {
startedProbe.tell("started!");
})
.build();
}
@Override
public EventHandler<Object, String> eventHandler() {
return newEventHandlerBuilder()
.forAnyState()
.onAnyEvent((event, state) -> state); // keep Integer state
}
}
@Test
public void failOnIncorrectExpectedStateForThenRun() {
TestProbe<String> probe = testKit.createTestProbe();
ActorRef<String> c =
testKit.spawn(
new IncorrectExpectedStateForThenRun(
probe.getRef(), PersistenceId.ofUniqueId("foiesftr")));
probe.expectMessage("started!");
LoggingTestKit.empty()
.withLogLevel(Level.ERROR)
// the error messages slightly changed in later JDKs
.withMessageRegex(
"(class )?java.lang.Integer cannot be cast to (class )?java.lang.String.*")
.expect(
testKit.system(),
() -> {
c.tell("expect wrong type");
return null;
});
probe.expectTerminated(c);
}
class SequenceNumberBehavior extends EventSourcedBehavior<String, String, String> {
private final ActorRef<String> probe;
private final ActorContext<String> context;
public SequenceNumberBehavior(
PersistenceId persistenceId, ActorRef<String> probe, ActorContext<String> context) {
super(persistenceId);
this.probe = probe;
this.context = context;
}
@Override
public String emptyState() {
return "";
}
@Override
public CommandHandler<String, String, String> commandHandler() {
return newCommandHandlerBuilder()
.forAnyState()
.onAnyCommand(
(state, cmd) -> {
probe.tell(lastSequenceNumber(context) + " onCommand");
return Effect()
.persist(cmd)
.thenRun((newState) -> probe.tell(lastSequenceNumber(context) + " thenRun"));
});
}
@Override
public EventHandler<String, String> eventHandler() {
return newEventHandlerBuilder()
.forAnyState()
.onAnyEvent(
(state, event) -> {
probe.tell(lastSequenceNumber(context) + " applyEvent");
return state + event;
});
}
@Override
public SignalHandler<String> signalHandler() {
return newSignalHandlerBuilder()
.onSignal(
RecoveryCompleted.class,
(state, completed) -> {
probe.tell(lastSequenceNumber(context) + " onRecoveryCompleted");
})
.build();
}
}
@Test
public void accessLastSequenceNumber() {
TestProbe<String> probe = testKit.createTestProbe(String.class);
ActorRef<String> ref =
testKit.spawn(
Behaviors.<String>setup(
context ->
new SequenceNumberBehavior(
PersistenceId.ofUniqueId("seqnr1"), probe.getRef(), context)));
probe.expectMessage("0 onRecoveryCompleted");
ref.tell("cmd");
probe.expectMessage("0 onCommand");
probe.expectMessage("1 applyEvent");
probe.expectMessage("1 thenRun");
}
}

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Silence initial setup logging from Logback -->
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{ISO8601} [%-5level] [%logger] [%marker] [%X{akkaSource}] [%X{persistencePhase}] [%X{persistenceId}] - %msg %n</pattern>
</encoder>
</appender>
<!--
Logging from tests are silenced by this appender. When there is a test failure
the captured logging events are flushed to the appenders defined for the
org.apache.pekko.actor.testkit.typed.internal.CapturingAppenderDelegate logger.
-->
<appender name="CapturingAppender" class="org.apache.pekko.actor.testkit.typed.internal.CapturingAppender" />
<!--
The appenders defined for this CapturingAppenderDelegate logger are used
when there is a test failure and all logging events from the test are
flushed to these appenders.
-->
<logger name="org.apache.pekko.actor.testkit.typed.internal.CapturingAppenderDelegate" >
<appender-ref ref="STDOUT"/>
</logger>
<root level="TRACE">
<appender-ref ref="CapturingAppender"/>
</root>
</configuration>

View file

@ -0,0 +1,337 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package docs.org.apache.pekko.persistence.typed
import java.time.Instant
import scala.concurrent.duration._
import docs.org.apache.pekko.persistence.typed.ReplicatedAuctionExampleSpec.AuctionEntity
import org.scalatest.wordspec.AnyWordSpecLike
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.ActorContext
import pekko.actor.typed.scaladsl.Behaviors
import pekko.actor.typed.scaladsl.LoggerOps
import pekko.actor.typed.scaladsl.TimerScheduler
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.RecoveryCompleted
import pekko.persistence.typed.ReplicaId
import pekko.persistence.typed.ReplicationId
import pekko.persistence.typed.scaladsl.Effect
import pekko.persistence.typed.scaladsl.EventSourcedBehavior
import pekko.persistence.typed.scaladsl.ReplicatedEventSourcing
import pekko.persistence.typed.scaladsl.ReplicationContext
import pekko.serialization.jackson.CborSerializable
object ReplicatedAuctionExampleSpec {
// #setup
object AuctionEntity {
// #setup
// #commands
type MoneyAmount = Int
case class Bid(bidder: String, offer: MoneyAmount, timestamp: Instant, originReplica: ReplicaId)
sealed trait Command extends CborSerializable
case object Finish extends Command // A timer needs to schedule this event at each replica
final case class OfferBid(bidder: String, offer: MoneyAmount) extends Command
final case class GetHighestBid(replyTo: ActorRef[Bid]) extends Command
final case class IsClosed(replyTo: ActorRef[Boolean]) extends Command
private case object Close extends Command // Internal, should not be sent from the outside
// #commands
// #events
sealed trait Event extends CborSerializable
final case class BidRegistered(bid: Bid) extends Event
final case class AuctionFinished(atReplica: ReplicaId) extends Event
final case class WinnerDecided(atReplica: ReplicaId, winningBid: Bid, highestCounterOffer: MoneyAmount)
extends Event
// #events
// #phase
/**
* The auction passes through several workflow phases.
* First, in `Running` `OfferBid` commands are accepted.
*
* `AuctionEntity` instances in all DCs schedule a `Finish` command
* at a given time. That persists the `AuctionFinished` event and the
* phase is in `Closing` until the auction is finished in all DCs.
*
* When the auction has been finished no more `OfferBid` commands are accepted.
*
* The auction is also finished immediately if `AuctionFinished` event from another
* DC is seen before the scheduled `Finish` command. In that way the auction is finished
* as quickly as possible in all DCs even though there might be some clock skew.
*
* One DC is responsible for finally deciding the winner and publishing the result.
* All events must be collected from all DC before that can happen.
* When the responsible DC has seen all `AuctionFinished` events from other DCs
* all other events have also been propagated and it can persist `WinnerDecided` and
* the auction is finally `Closed`.
*/
sealed trait AuctionPhase
case object Running extends AuctionPhase
final case class Closing(finishedAtReplica: Set[ReplicaId]) extends AuctionPhase
case object Closed extends AuctionPhase
// #phase
// #state
case class AuctionState(phase: AuctionPhase, highestBid: Bid, highestCounterOffer: MoneyAmount)
extends CborSerializable {
def applyEvent(event: Event): AuctionState =
event match {
case BidRegistered(b) =>
if (isHigherBid(b, highestBid))
withNewHighestBid(b)
else
withTooLowBid(b)
case AuctionFinished(atDc) =>
phase match {
case Running =>
copy(phase = Closing(Set(atDc)))
case Closing(alreadyFinishedDcs) =>
copy(phase = Closing(alreadyFinishedDcs + atDc))
case _ =>
this
}
case _: WinnerDecided =>
copy(phase = Closed)
}
def withNewHighestBid(bid: Bid): AuctionState = {
require(phase != Closed)
require(isHigherBid(bid, highestBid))
copy(highestBid = bid, highestCounterOffer = highestBid.offer // keep last highest bid around
)
}
def withTooLowBid(bid: Bid): AuctionState = {
require(phase != Closed)
require(isHigherBid(highestBid, bid))
copy(highestCounterOffer = highestCounterOffer.max(bid.offer)) // update highest counter offer
}
def isHigherBid(first: Bid, second: Bid): Boolean =
first.offer > second.offer ||
(first.offer == second.offer && first.timestamp.isBefore(second.timestamp)) || // if equal, first one wins
// If timestamps are equal, choose by dc where the offer was submitted
// In real auctions, this last comparison should be deterministic but unpredictable, so that submitting to a
// particular DC would not be an advantage.
(first.offer == second.offer && first.timestamp.equals(second.timestamp) && first.originReplica.id
.compareTo(second.originReplica.id) < 0)
}
// #state
// #setup
def apply(
replica: ReplicaId,
name: String,
initialBid: AuctionEntity.Bid, // the initial bid is basically the minimum price bidden at start time by the owner
closingAt: Instant,
responsibleForClosing: Boolean,
allReplicas: Set[ReplicaId]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
Behaviors.withTimers { timers =>
ReplicatedEventSourcing.commonJournalConfig(
ReplicationId("auction", name, replica),
allReplicas,
PersistenceTestKitReadJournal.Identifier) { replicationCtx =>
new AuctionEntity(ctx, replicationCtx, timers, closingAt, responsibleForClosing, allReplicas)
.behavior(initialBid)
}
}
}
}
class AuctionEntity(
context: ActorContext[AuctionEntity.Command],
replicationContext: ReplicationContext,
timers: TimerScheduler[AuctionEntity.Command],
closingAt: Instant,
responsibleForClosing: Boolean,
allReplicas: Set[ReplicaId]) {
import AuctionEntity._
private def behavior(initialBid: AuctionEntity.Bid): EventSourcedBehavior[Command, Event, AuctionState] =
EventSourcedBehavior(
replicationContext.persistenceId,
AuctionState(phase = Running, highestBid = initialBid, highestCounterOffer = initialBid.offer),
commandHandler,
eventHandler).receiveSignal {
case (state, RecoveryCompleted) => recoveryCompleted(state)
}
private def recoveryCompleted(state: AuctionState): Unit = {
if (shouldClose(state))
context.self ! Close
val millisUntilClosing = closingAt.toEpochMilli - replicationContext.currentTimeMillis()
timers.startSingleTimer(Finish, millisUntilClosing.millis)
}
// #setup
// #command-handler
def commandHandler(state: AuctionState, command: Command): Effect[Event, AuctionState] = {
state.phase match {
case Closing(_) | Closed =>
command match {
case GetHighestBid(replyTo) =>
replyTo ! state.highestBid.copy(offer = state.highestCounterOffer) // TODO this is not as described
Effect.none
case IsClosed(replyTo) =>
replyTo ! (state.phase == Closed)
Effect.none
case Finish =>
context.log.info("Finish")
Effect.persist(AuctionFinished(replicationContext.replicaId))
case Close =>
context.log.info("Close")
require(shouldClose(state))
// TODO send email (before or after persisting)
Effect.persist(WinnerDecided(replicationContext.replicaId, state.highestBid, state.highestCounterOffer))
case _: OfferBid =>
// auction finished, no more bids accepted
Effect.unhandled
}
case Running =>
command match {
case OfferBid(bidder, offer) =>
Effect.persist(
BidRegistered(
Bid(
bidder,
offer,
Instant.ofEpochMilli(replicationContext.currentTimeMillis()),
replicationContext.replicaId)))
case GetHighestBid(replyTo) =>
replyTo ! state.highestBid
Effect.none
case Finish =>
Effect.persist(AuctionFinished(replicationContext.replicaId))
case Close =>
context.log.warn("Premature close")
// Close should only be triggered when we have already finished
Effect.unhandled
case IsClosed(replyTo) =>
replyTo ! false
Effect.none
}
}
}
// #command-handler
// #event-handler
def eventHandler(state: AuctionState, event: Event): AuctionState = {
val newState = state.applyEvent(event)
context.log.infoN("Applying event {}. New start {}", event, newState)
if (!replicationContext.recoveryRunning) {
eventTriggers(event, newState)
}
newState
}
// #event-handler
// #event-triggers
private def eventTriggers(event: Event, newState: AuctionState): Unit = {
event match {
case finished: AuctionFinished =>
newState.phase match {
case Closing(alreadyFinishedAtDc) =>
context.log.infoN(
"AuctionFinished at {}, already finished at [{}]",
finished.atReplica,
alreadyFinishedAtDc.mkString(", "))
if (alreadyFinishedAtDc(replicationContext.replicaId)) {
if (shouldClose(newState)) context.self ! Close
} else {
context.log.info("Sending finish to self")
context.self ! Finish
}
case _ => // no trigger for this state
}
case _ => // no trigger for this event
}
}
private def shouldClose(state: AuctionState): Boolean = {
responsibleForClosing && (state.phase match {
case Closing(alreadyFinishedAtDc) =>
val allDone = allReplicas.diff(alreadyFinishedAtDc).isEmpty
if (!allDone) {
context.log.info2(
s"Not closing auction as not all DCs have reported finished. All DCs: {}. Reported finished {}",
allReplicas,
alreadyFinishedAtDc)
}
allDone
case _ =>
false
})
}
// #event-triggers
// #setup
}
// #setup
}
class ReplicatedAuctionExampleSpec
extends ScalaTestWithActorTestKit(PersistenceTestKitPlugin.config)
with AnyWordSpecLike
with LogCapturing {
import ReplicatedAuctionExampleSpec.AuctionEntity._
"Auction example" should {
"work" in {
val Replicas = Set(ReplicaId("DC-A"), ReplicaId("DC-B"))
val auctionName = "old-skis"
val initialBid = Bid("chbatey", 12, Instant.now(), ReplicaId("DC-A"))
val closingAt = Instant.now().plusSeconds(10)
val dcAReplica: ActorRef[Command] = spawn(
AuctionEntity(ReplicaId("DC-A"), auctionName, initialBid, closingAt, responsibleForClosing = true, Replicas))
val dcBReplica: ActorRef[Command] = spawn(
AuctionEntity(ReplicaId("DC-B"), auctionName, initialBid, closingAt, responsibleForClosing = false, Replicas))
dcAReplica ! OfferBid("me", 100)
dcAReplica ! OfferBid("me", 99)
dcAReplica ! OfferBid("me", 202)
eventually {
val replyProbe = createTestProbe[Bid]()
dcAReplica ! GetHighestBid(replyProbe.ref)
val bid = replyProbe.expectMessageType[Bid]
bid.offer shouldEqual 202
}
dcAReplica ! Finish
eventually {
val finishProbe = createTestProbe[Boolean]()
dcAReplica ! IsClosed(finishProbe.ref)
finishProbe.expectMessage(true)
}
eventually {
val finishProbe = createTestProbe[Boolean]()
dcBReplica ! IsClosed(finishProbe.ref)
finishProbe.expectMessage(true)
}
}
}
}

View file

@ -0,0 +1,171 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package docs.org.apache.pekko.persistence.typed
import org.scalatest.wordspec.AnyWordSpecLike
import org.apache.pekko
import pekko.Done
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.ActorContext
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.ReplicaId
import pekko.persistence.typed.ReplicationId
import pekko.persistence.typed.crdt.LwwTime
import pekko.persistence.typed.scaladsl._
import pekko.serialization.jackson.CborSerializable
object ReplicatedBlogExampleSpec {
object BlogEntity {
object BlogState {
val empty: BlogState = BlogState(None, LwwTime(Long.MinValue, ReplicaId("")), published = false)
}
final case class BlogState(content: Option[PostContent], contentTimestamp: LwwTime, published: Boolean)
extends CborSerializable {
def withContent(newContent: PostContent, timestamp: LwwTime): BlogState =
copy(content = Some(newContent), contentTimestamp = timestamp)
def isEmpty: Boolean = content.isEmpty
}
final case class PostContent(title: String, body: String) extends CborSerializable
final case class Published(postId: String) extends Event
sealed trait Command extends CborSerializable
final case class AddPost(postId: String, content: PostContent, replyTo: ActorRef[AddPostDone]) extends Command
final case class AddPostDone(postId: String)
final case class GetPost(postId: String, replyTo: ActorRef[PostContent]) extends Command
final case class ChangeBody(postId: String, newContent: PostContent, replyTo: ActorRef[Done]) extends Command
final case class Publish(postId: String, replyTo: ActorRef[Done]) extends Command
sealed trait Event extends CborSerializable
final case class PostAdded(postId: String, content: PostContent, timestamp: LwwTime) extends Event
final case class BodyChanged(postId: String, newContent: PostContent, timestamp: LwwTime) extends Event
def apply(entityId: String, replicaId: ReplicaId, allReplicaIds: Set[ReplicaId]): Behavior[Command] = {
Behaviors.setup[Command] { ctx =>
ReplicatedEventSourcing.commonJournalConfig(
ReplicationId("blog", entityId, replicaId),
allReplicaIds,
PersistenceTestKitReadJournal.Identifier) { replicationContext =>
EventSourcedBehavior[Command, Event, BlogState](
replicationContext.persistenceId,
BlogState.empty,
(state, cmd) => commandHandler(ctx, replicationContext, state, cmd),
(state, event) => eventHandler(ctx, replicationContext, state, event))
}
}
}
// #command-handler
private def commandHandler(
ctx: ActorContext[Command],
replicationContext: ReplicationContext,
state: BlogState,
cmd: Command): Effect[Event, BlogState] = {
cmd match {
case AddPost(_, content, replyTo) =>
val evt =
PostAdded(
replicationContext.entityId,
content,
state.contentTimestamp.increase(replicationContext.currentTimeMillis(), replicationContext.replicaId))
Effect.persist(evt).thenRun { _ =>
replyTo ! AddPostDone(replicationContext.entityId)
}
case ChangeBody(_, newContent, replyTo) =>
val evt =
BodyChanged(
replicationContext.entityId,
newContent,
state.contentTimestamp.increase(replicationContext.currentTimeMillis(), replicationContext.replicaId))
Effect.persist(evt).thenRun { _ =>
replyTo ! Done
}
case p: Publish =>
Effect.persist(Published("id")).thenRun { _ =>
p.replyTo ! Done
}
case gp: GetPost =>
ctx.log.info("GetPost {}", state.content)
state.content.foreach(content => gp.replyTo ! content)
Effect.none
}
}
// #command-handler
// #event-handler
private def eventHandler(
ctx: ActorContext[Command],
replicationContext: ReplicationContext,
state: BlogState,
event: Event): BlogState = {
ctx.log.info(s"${replicationContext.entityId}:${replicationContext.replicaId} Received event $event")
event match {
case PostAdded(_, content, timestamp) =>
if (timestamp.isAfter(state.contentTimestamp)) {
val s = state.withContent(content, timestamp)
ctx.log.info("Updating content. New content is {}", s)
s
} else {
ctx.log.info("Ignoring event as timestamp is older")
state
}
case BodyChanged(_, newContent, timestamp) =>
if (timestamp.isAfter(state.contentTimestamp))
state.withContent(newContent, timestamp)
else state
case Published(_) =>
state.copy(published = true)
}
}
// #event-handler
}
}
class ReplicatedBlogExampleSpec
extends ScalaTestWithActorTestKit(PersistenceTestKitPlugin.config)
with AnyWordSpecLike
with LogCapturing {
import ReplicatedBlogExampleSpec.BlogEntity
import ReplicatedBlogExampleSpec.BlogEntity._
"Blog Example" should {
"work" in {
val refDcA: ActorRef[Command] =
spawn(BlogEntity("cat", ReplicaId("DC-A"), Set(ReplicaId("DC-A"), ReplicaId("DC-B"))))
val refDcB: ActorRef[Command] =
spawn(BlogEntity("cat", ReplicaId("DC-B"), Set(ReplicaId("DC-A"), ReplicaId("DC-B"))))
import scala.concurrent.duration._
import pekko.actor.typed.scaladsl.AskPattern._
import pekko.util.Timeout
implicit val timeout: Timeout = 3.seconds
val content = PostContent("cats are the bets", "yep")
val response =
refDcA.ask[AddPostDone](replyTo => AddPost("cat", content, replyTo)).futureValue
response shouldEqual AddPostDone("cat")
eventually {
refDcA.ask[PostContent](replyTo => GetPost("cat", replyTo)).futureValue shouldEqual content
}
eventually {
refDcB.ask[PostContent](replyTo => GetPost("cat", replyTo)).futureValue shouldEqual content
}
}
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package docs.org.apache.pekko.persistence.typed
import scala.annotation.nowarn
import org.apache.pekko
import pekko.actor.typed.ActorSystem
import pekko.persistence.typed.ReplicaId
import pekko.persistence.typed.ReplicationId
import pekko.persistence.typed.scaladsl.EventSourcedBehavior
import pekko.persistence.typed.scaladsl.ReplicatedEventSourcing
@nowarn("msg=never used")
object ReplicatedEventSourcingCompileOnlySpec {
// #replicas
val DCA = ReplicaId("DC-A")
val DCB = ReplicaId("DC-B")
val AllReplicas = Set(DCA, DCB)
// #replicas
val queryPluginId = ""
trait Command
trait State
trait Event
object Shared {
// #factory-shared
def apply(
system: ActorSystem[_],
entityId: String,
replicaId: ReplicaId): EventSourcedBehavior[Command, State, Event] = {
ReplicatedEventSourcing.commonJournalConfig(
ReplicationId("MyReplicatedEntity", entityId, replicaId),
AllReplicas,
queryPluginId) { replicationContext =>
EventSourcedBehavior[Command, State, Event](???, ???, ???, ???)
}
}
// #factory-shared
}
object PerReplica {
// #factory
def apply(
system: ActorSystem[_],
entityId: String,
replicaId: ReplicaId): EventSourcedBehavior[Command, State, Event] = {
ReplicatedEventSourcing.perReplicaJournalConfig(
ReplicationId("MyReplicatedEntity", entityId, replicaId),
Map(DCA -> "journalForDCA", DCB -> "journalForDCB")) { replicationContext =>
EventSourcedBehavior[Command, State, Event](???, ???, ???, ???)
}
}
// #factory
}
}

View file

@ -0,0 +1,105 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package docs.org.apache.pekko.persistence.typed
import org.scalatest.wordspec.AnyWordSpecLike
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.ReplicaId
import pekko.persistence.typed.ReplicationId
import pekko.persistence.typed.crdt.ORSet
import pekko.persistence.typed.scaladsl.Effect
import pekko.persistence.typed.scaladsl.EventSourcedBehavior
import pekko.persistence.typed.scaladsl.ReplicatedEventSourcing
object ReplicatedMovieWatchListExampleSpec {
// #movie-entity
object MovieWatchList {
sealed trait Command
final case class AddMovie(movieId: String) extends Command
final case class RemoveMovie(movieId: String) extends Command
final case class GetMovieList(replyTo: ActorRef[MovieList]) extends Command
final case class MovieList(movieIds: Set[String])
def apply(entityId: String, replicaId: ReplicaId, allReplicaIds: Set[ReplicaId]): Behavior[Command] = {
ReplicatedEventSourcing.commonJournalConfig(
ReplicationId("movies", entityId, replicaId),
allReplicaIds,
PersistenceTestKitReadJournal.Identifier) { replicationContext =>
EventSourcedBehavior[Command, ORSet.DeltaOp, ORSet[String]](
replicationContext.persistenceId,
ORSet.empty(replicationContext.replicaId),
(state, cmd) => commandHandler(state, cmd),
(state, event) => eventHandler(state, event))
}
}
private def commandHandler(state: ORSet[String], cmd: Command): Effect[ORSet.DeltaOp, ORSet[String]] = {
cmd match {
case AddMovie(movieId) =>
Effect.persist(state + movieId)
case RemoveMovie(movieId) =>
Effect.persist(state - movieId)
case GetMovieList(replyTo) =>
replyTo ! MovieList(state.elements)
Effect.none
}
}
private def eventHandler(state: ORSet[String], event: ORSet.DeltaOp): ORSet[String] = {
state.applyOperation(event)
}
}
// #movie-entity
}
class ReplicatedMovieWatchListExampleSpec
extends ScalaTestWithActorTestKit(PersistenceTestKitPlugin.config)
with AnyWordSpecLike
with LogCapturing {
import ReplicatedMovieWatchListExampleSpec._
"MovieWatchList" must {
"demonstrate ORSet" in {
import MovieWatchList._
val Replicas = Set(ReplicaId("DC-A"), ReplicaId("DC-B"))
val dcAReplica: ActorRef[Command] = spawn(MovieWatchList("mylist", ReplicaId("DC-A"), Replicas))
val dcBReplica: ActorRef[Command] = spawn(MovieWatchList("mylist", ReplicaId("DC-B"), Replicas))
val probeA = createTestProbe[MovieList]()
val probeB = createTestProbe[MovieList]()
dcAReplica ! AddMovie("movie-15")
dcAReplica ! AddMovie("movie-17")
dcBReplica ! AddMovie("movie-20")
eventually {
dcAReplica ! GetMovieList(probeA.ref)
probeA.expectMessage(MovieList(Set("movie-15", "movie-17", "movie-20")))
dcBReplica ! GetMovieList(probeB.ref)
probeB.expectMessage(MovieList(Set("movie-15", "movie-17", "movie-20")))
}
dcBReplica ! RemoveMovie("movie-17")
eventually {
dcAReplica ! GetMovieList(probeA.ref)
probeA.expectMessage(MovieList(Set("movie-15", "movie-20")))
dcBReplica ! GetMovieList(probeB.ref)
probeB.expectMessage(MovieList(Set("movie-15", "movie-20")))
}
}
}
}

View file

@ -0,0 +1,122 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package docs.org.apache.pekko.persistence.typed
import java.util.UUID
import docs.org.apache.pekko.persistence.typed.ReplicatedShoppingCartExampleSpec.ShoppingCart.CartItems
import org.scalatest.wordspec.AnyWordSpecLike
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.ReplicaId
import pekko.persistence.typed.ReplicationId
import pekko.persistence.typed.crdt.Counter
import pekko.persistence.typed.scaladsl.Effect
import pekko.persistence.typed.scaladsl.EventSourcedBehavior
import pekko.persistence.typed.scaladsl.ReplicatedEventSourcing
import pekko.serialization.jackson.CborSerializable
object ReplicatedShoppingCartExampleSpec {
// #shopping-cart
object ShoppingCart {
type ProductId = String
sealed trait Command extends CborSerializable
final case class AddItem(id: ProductId, count: Int) extends Command
final case class RemoveItem(id: ProductId, count: Int) extends Command
final case class GetCartItems(replyTo: ActorRef[CartItems]) extends Command
final case class CartItems(items: Map[ProductId, Int]) extends CborSerializable
sealed trait Event extends CborSerializable
final case class ItemUpdated(id: ProductId, update: Counter.Updated) extends Event
final case class State(items: Map[ProductId, Counter])
def apply(entityId: String, replicaId: ReplicaId, allReplicaIds: Set[ReplicaId]): Behavior[Command] = {
ReplicatedEventSourcing.commonJournalConfig(
ReplicationId("blog", entityId, replicaId),
allReplicaIds,
PersistenceTestKitReadJournal.Identifier) { replicationContext =>
EventSourcedBehavior[Command, Event, State](
replicationContext.persistenceId,
State(Map.empty),
(state, cmd) => commandHandler(state, cmd),
(state, event) => eventHandler(state, event))
}
}
private def commandHandler(state: State, cmd: Command): Effect[Event, State] = {
cmd match {
case AddItem(productId, count) =>
Effect.persist(ItemUpdated(productId, Counter.Updated(count)))
case RemoveItem(productId, count) =>
Effect.persist(ItemUpdated(productId, Counter.Updated(-count)))
case GetCartItems(replyTo) =>
val items = state.items.collect {
case (id, counter) if counter.value > 0 => id -> counter.value.toInt
}
replyTo ! CartItems(items)
Effect.none
}
}
private def eventHandler(state: State, event: Event): State = {
event match {
case ItemUpdated(id, update) =>
val newItems = state.items.get(id) match {
case Some(counter) => state.items + (id -> counter.applyOperation(update))
case None => state.items + (id -> Counter.empty.applyOperation(update))
}
State(newItems)
}
}
}
// #shopping-cart
}
class ReplicatedShoppingCartExampleSpec
extends ScalaTestWithActorTestKit(PersistenceTestKitPlugin.config)
with AnyWordSpecLike
with LogCapturing {
import ReplicatedShoppingCartExampleSpec.ShoppingCart
"Replicated shopping cart" should {
"work" in {
val cartId = UUID.randomUUID().toString
val refDcA: ActorRef[ShoppingCart.Command] =
spawn(ShoppingCart(cartId, ReplicaId("DC-A"), Set(ReplicaId("DC-A"), ReplicaId("DC-B"))))
val refDcB: ActorRef[ShoppingCart.Command] =
spawn(ShoppingCart(cartId, ReplicaId("DC-B"), Set(ReplicaId("DC-A"), ReplicaId("DC-B"))))
val fidgetSpinnerId = "T2912"
val rubicsCubeId = "T1302"
refDcA ! ShoppingCart.AddItem(fidgetSpinnerId, 10)
refDcB ! ShoppingCart.AddItem(rubicsCubeId, 10)
refDcA ! ShoppingCart.AddItem(rubicsCubeId, 10)
refDcA ! ShoppingCart.AddItem(fidgetSpinnerId, 10)
refDcB ! ShoppingCart.AddItem(fidgetSpinnerId, 10)
refDcA ! ShoppingCart.RemoveItem(fidgetSpinnerId, 10)
refDcA ! ShoppingCart.AddItem(rubicsCubeId, 10)
refDcB ! ShoppingCart.RemoveItem(rubicsCubeId, 10)
val replyProbe = createTestProbe[CartItems]()
eventually {
refDcA ! ShoppingCart.GetCartItems(replyProbe.ref)
replyProbe.expectMessage(CartItems(Map(fidgetSpinnerId -> 20, rubicsCubeId -> 20)))
}
}
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed
import org.apache.pekko
import pekko.Done
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.eventstream.EventStream
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.typed.scaladsl.Effect
import pekko.persistence.typed.scaladsl.EventSourcedBehavior
import pekko.serialization.jackson.CborSerializable
import org.scalatest.wordspec.AnyWordSpecLike
object EventPublishingSpec {
object WowSuchEventSourcingBehavior {
sealed trait Command
case class StoreThis(data: String, tagIt: Boolean, replyTo: ActorRef[Done]) extends Command
final case class Event(data: String, tagIt: Boolean) extends CborSerializable
def apply(id: PersistenceId): Behavior[Command] =
EventSourcedBehavior[Command, Event, Set[Event]](
id,
Set.empty,
(_, command) =>
command match {
case StoreThis(data, tagIt, replyTo) =>
Effect.persist(Event(data, tagIt)).thenRun(_ => replyTo ! Done)
},
(state, event) => state + event)
.withTagger(evt => if (evt.tagIt) Set("tag") else Set.empty)
.withEventPublishing(enabled = true)
}
}
class EventPublishingSpec
extends ScalaTestWithActorTestKit(PersistenceTestKitPlugin.config)
with AnyWordSpecLike
with LogCapturing {
import EventPublishingSpec._
"EventPublishing support" must {
"publish events after written for any actor" in {
val topicProbe = createTestProbe[PublishedEvent]()
system.eventStream ! EventStream.Subscribe(topicProbe.ref)
// We don't verify subscription completed (no ack available), but expect the next steps to take enough time
// for subscription to complete
val myId = PersistenceId.ofUniqueId("myId")
val wowSuchActor = spawn(WowSuchEventSourcingBehavior(myId))
val persistProbe = createTestProbe[Any]()
wowSuchActor ! WowSuchEventSourcingBehavior.StoreThis("great stuff", tagIt = false, replyTo = persistProbe.ref)
persistProbe.expectMessage(Done)
val published1 = topicProbe.receiveMessage()
published1.persistenceId should ===(myId)
published1.event should ===(WowSuchEventSourcingBehavior.Event("great stuff", false))
published1.sequenceNumber should ===(1L)
published1.tags should ===(Set.empty)
val anotherId = PersistenceId.ofUniqueId("anotherId")
val anotherActor = spawn(WowSuchEventSourcingBehavior(anotherId))
anotherActor ! WowSuchEventSourcingBehavior.StoreThis("another event", tagIt = true, replyTo = persistProbe.ref)
persistProbe.expectMessage(Done)
val published2 = topicProbe.receiveMessage()
published2.persistenceId should ===(anotherId)
published2.event should ===(WowSuchEventSourcingBehavior.Event("another event", true))
published2.sequenceNumber should ===(1L)
published2.tags should ===(Set("tag"))
}
}
}

View file

@ -0,0 +1,138 @@
/*
* Copyright (C) 2021-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.LoggingTestKit
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.typed.EventSourcedBehaviorLoggingSpec.ChattyEventSourcingBehavior.Hello
import pekko.persistence.typed.EventSourcedBehaviorLoggingSpec.ChattyEventSourcingBehavior.Hellos
import pekko.persistence.typed.scaladsl.Effect
import pekko.persistence.typed.scaladsl.EventSourcedBehavior
import pekko.serialization.jackson.CborSerializable
import com.typesafe.config.{ Config, ConfigFactory }
import org.scalatest.wordspec.AnyWordSpecLike
import pekko.Done
import pekko.actor.typed.ActorRef
object EventSourcedBehaviorLoggingSpec {
object ChattyEventSourcingBehavior {
sealed trait Command
case class Hello(msg: String, replyTo: ActorRef[Done]) extends Command
case class Hellos(msg1: String, msg2: String, replyTo: ActorRef[Done]) extends Command
final case class Event(msg: String) extends CborSerializable
def apply(id: PersistenceId): Behavior[Command] = {
Behaviors.setup { ctx =>
EventSourcedBehavior[Command, Event, Set[Event]](
id,
Set.empty,
(_, command) =>
command match {
case Hello(msg, replyTo) =>
ctx.log.info("received message '{}'", msg)
Effect.persist(Event(msg)).thenReply(replyTo)(_ => Done)
case Hellos(msg1, msg2, replyTo) =>
Effect.persist(Event(msg1), Event(msg2)).thenReply(replyTo)(_ => Done)
},
(state, event) => state + event)
}
}
}
}
abstract class EventSourcedBehaviorLoggingSpec(config: Config)
extends ScalaTestWithActorTestKit(config)
with AnyWordSpecLike
with LogCapturing {
import EventSourcedBehaviorLoggingSpec._
def loggerName: String
def loggerId: String
s"Chatty behavior ($loggerId)" must {
val myId = PersistenceId("Chatty", "chat-1")
val chattyActor = spawn(ChattyEventSourcingBehavior(myId))
"always log user message in context.log" in {
val doneProbe = createTestProbe[Done]()
LoggingTestKit
.info("received message 'Mary'")
.withLoggerName(
"org.apache.pekko.persistence.typed.EventSourcedBehaviorLoggingSpec$ChattyEventSourcingBehavior$")
.expect {
chattyActor ! Hello("Mary", doneProbe.ref)
doneProbe.receiveMessage()
}
}
s"log internal messages in '$loggerId' logger without logging user data (Persist)" in {
val doneProbe = createTestProbe[Done]()
LoggingTestKit
.debug(
"Handled command [org.apache.pekko.persistence.typed.EventSourcedBehaviorLoggingSpec$ChattyEventSourcingBehavior$Hello], " +
"resulting effect: [Persist(org.apache.pekko.persistence.typed.EventSourcedBehaviorLoggingSpec$ChattyEventSourcingBehavior$Event)], side effects: [1]")
.withLoggerName(loggerName)
.expect {
chattyActor ! Hello("Joe", doneProbe.ref)
doneProbe.receiveMessage()
}
}
s"log internal messages in '$loggerId' logger without logging user data (PersistAll)" in {
val doneProbe = createTestProbe[Done]()
LoggingTestKit
.debug(
"Handled command [org.apache.pekko.persistence.typed.EventSourcedBehaviorLoggingSpec$ChattyEventSourcingBehavior$Hellos], " +
"resulting effect: [PersistAll(org.apache.pekko.persistence.typed.EventSourcedBehaviorLoggingSpec$ChattyEventSourcingBehavior$Event," +
"org.apache.pekko.persistence.typed.EventSourcedBehaviorLoggingSpec$ChattyEventSourcingBehavior$Event)], side effects: [1]")
.withLoggerName(loggerName)
.expect {
chattyActor ! Hellos("Mary", "Joe", doneProbe.ref)
doneProbe.receiveMessage()
}
}
s"log in '$loggerId' while preserving MDC source" in {
val doneProbe = createTestProbe[Done]()
LoggingTestKit
.debug("Handled command ")
.withLoggerName(loggerName)
.withMdc(Map("persistencePhase" -> "running-cmd", "persistenceId" -> "Chatty|chat-1"))
.expect {
chattyActor ! Hello("Mary", doneProbe.ref)
doneProbe.receiveMessage()
}
}
}
}
class EventSourcedBehaviorLoggingInternalLoggerSpec
extends EventSourcedBehaviorLoggingSpec(PersistenceTestKitPlugin.config) {
override def loggerName = "org.apache.pekko.persistence.typed.internal.EventSourcedBehaviorImpl"
override def loggerId = "internal.log"
}
object EventSourcedBehaviorLoggingContextLoggerSpec {
val config =
ConfigFactory
.parseString("pekko.persistence.typed.use-context-logger-for-internal-logging = true")
.withFallback(PersistenceTestKitPlugin.config)
}
class EventSourcedBehaviorLoggingContextLoggerSpec
extends EventSourcedBehaviorLoggingSpec(EventSourcedBehaviorLoggingContextLoggerSpec.config) {
override def loggerName =
"org.apache.pekko.persistence.typed.EventSourcedBehaviorLoggingSpec$ChattyEventSourcingBehavior$"
override def loggerId = "context.log"
}

View file

@ -0,0 +1,118 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed
import java.util.concurrent.atomic.AtomicInteger
import org.apache.pekko
import pekko.Done
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.persistence.query.PersistenceQuery
import pekko.persistence.query.scaladsl.CurrentEventsByPersistenceIdQuery
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.typed.scaladsl.ReplicatedEventSourcing
import pekko.persistence.typed.scaladsl.Effect
import pekko.persistence.typed.scaladsl.EventSourcedBehavior
import pekko.stream.scaladsl.Sink
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import org.scalatest.concurrent.Eventually
import org.scalatest.wordspec.AnyWordSpecLike
object MultiJournalReplicationSpec {
object Actor {
sealed trait Command
case class GetState(replyTo: ActorRef[Set[String]]) extends Command
case class StoreMe(text: String, ack: ActorRef[Done]) extends Command
private val writeJournalPerReplica = Map("R1" -> "journal1.journal", "R2" -> "journal2.journal")
def apply(entityId: String, replicaId: String): Behavior[Command] = {
ReplicatedEventSourcing
.perReplicaJournalConfig(
ReplicationId("MultiJournalSpec", entityId, ReplicaId(replicaId)),
Map(ReplicaId("R1") -> "journal1.query", ReplicaId("R2") -> "journal2.query"))(replicationContext =>
EventSourcedBehavior[Command, String, Set[String]](
replicationContext.persistenceId,
Set.empty[String],
(state, command) =>
command match {
case GetState(replyTo) =>
replyTo ! state
Effect.none
case StoreMe(evt, ack) =>
Effect.persist(evt).thenRun(_ => ack ! Done)
},
(state, event) => state + event))
.withJournalPluginId(writeJournalPerReplica(replicaId))
}
}
def separateJournalsConfig: Config = ConfigFactory.parseString(s"""
journal1 {
journal.class = "${classOf[PersistenceTestKitPlugin].getName}"
query = $${pekko.persistence.testkit.query}
}
journal2 {
journal.class = "${classOf[PersistenceTestKitPlugin].getName}"
query = $${pekko.persistence.testkit.query}
}
""").withFallback(ConfigFactory.load()).resolve()
}
class MultiJournalReplicationSpec
extends ScalaTestWithActorTestKit(MultiJournalReplicationSpec.separateJournalsConfig)
with AnyWordSpecLike
with LogCapturing
with Eventually {
import MultiJournalReplicationSpec._
val ids = new AtomicInteger(0)
def nextEntityId = s"e-${ids.getAndIncrement()}"
"ReplicatedEventSourcing" should {
"support one journal per replica" in {
val r1 = spawn(Actor("id1", "R1"))
val r2 = spawn(Actor("id1", "R2"))
val probe = createTestProbe[Any]()
r1 ! Actor.StoreMe("r1 m1", probe.ref)
probe.expectMessage(Done)
r2 ! Actor.StoreMe("r2 m1", probe.ref)
probe.expectMessage(Done)
eventually {
val probe = createTestProbe[Set[String]]()
r1 ! Actor.GetState(probe.ref)
probe.receiveMessage() should ===(Set("r1 m1", "r2 m1"))
r2 ! Actor.GetState(probe.ref)
probe.receiveMessage() should ===(Set("r1 m1", "r2 m1"))
}
val readJournal1 = PersistenceQuery(system).readJournalFor[CurrentEventsByPersistenceIdQuery]("journal1.query")
val readJournal2 = PersistenceQuery(system).readJournalFor[CurrentEventsByPersistenceIdQuery]("journal2.query")
val eventsForJournal1 =
readJournal1
.currentEventsByPersistenceId("MultiJournalSpec|id1|R1", 0L, Long.MaxValue)
.runWith(Sink.seq)
.futureValue
eventsForJournal1.map(_.event).toSet should ===(Set("r1 m1", "r2 m1"))
val eventsForJournal2 =
readJournal2
.currentEventsByPersistenceId("MultiJournalSpec|id1|R2", 0L, Long.MaxValue)
.runWith(Sink.seq)
.futureValue
eventsForJournal2.map(_.event).toSet should ===(Set("r1 m1", "r2 m1"))
}
}
}

View file

@ -0,0 +1,241 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed
import org.apache.pekko
import pekko.Done
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.internal.{ ReplicatedPublishedEventMetaData, VersionVector }
import pekko.persistence.typed.scaladsl.ReplicatedEventSourcing
import pekko.persistence.typed.scaladsl.Effect
import pekko.persistence.typed.scaladsl.EventSourcedBehavior
import org.scalatest.wordspec.AnyWordSpecLike
object ReplicatedEventPublishingSpec {
val EntityType = "EventPublishingSpec"
object MyReplicatedBehavior {
trait Command
case class Add(text: String, replyTo: ActorRef[Done]) extends Command
case class Get(replyTo: ActorRef[Set[String]]) extends Command
case object Stop extends Command
def apply(entityId: String, replicaId: ReplicaId, allReplicas: Set[ReplicaId]): Behavior[Command] =
Behaviors.setup { ctx =>
ReplicatedEventSourcing.commonJournalConfig(
ReplicationId(EntityType, entityId, replicaId),
allReplicas,
PersistenceTestKitReadJournal.Identifier)(replicationContext =>
EventSourcedBehavior[Command, String, Set[String]](
replicationContext.persistenceId,
Set.empty,
(state, command) =>
command match {
case Add(string, replyTo) =>
ctx.log.debug("Persisting [{}]", string)
Effect.persist(string).thenRun { _ =>
ctx.log.debug("Ack:ing [{}]", string)
replyTo ! Done
}
case Get(replyTo) =>
replyTo ! state
Effect.none
case Stop =>
Effect.stop()
case unexpected => throw new RuntimeException(s"Unexpected: $unexpected")
},
(state, string) => state + string))
}
}
}
class ReplicatedEventPublishingSpec
extends ScalaTestWithActorTestKit(PersistenceTestKitPlugin.config)
with AnyWordSpecLike
with LogCapturing {
val DCA = ReplicaId("DC-A")
val DCB = ReplicaId("DC-B")
val DCC = ReplicaId("DC-C")
private var idCounter = 0
def nextEntityId(): String = {
idCounter += 1
s"myId$idCounter"
}
import ReplicatedEventPublishingSpec._
"An Replicated Event Sourced actor" must {
"move forward when a published event from a replica is received" in {
val id = nextEntityId()
val actor = spawn(MyReplicatedBehavior(id, DCA, Set(DCA, DCB)))
val probe = createTestProbe[Any]()
actor ! MyReplicatedBehavior.Add("one", probe.ref)
probe.expectMessage(Done)
// simulate a published event from another replica
actor.asInstanceOf[ActorRef[Any]] ! internal.PublishedEventImpl(
ReplicationId(EntityType, id, DCB).persistenceId,
1L,
"two",
System.currentTimeMillis(),
Some(new ReplicatedPublishedEventMetaData(DCB, VersionVector.empty)))
actor ! MyReplicatedBehavior.Add("three", probe.ref)
probe.expectMessage(Done)
actor ! MyReplicatedBehavior.Get(probe.ref)
probe.expectMessage(Set("one", "two", "three"))
}
"ignore a published event from a replica is received but the sequence number is unexpected" in {
val id = nextEntityId()
val actor = spawn(MyReplicatedBehavior(id, DCA, Set(DCA, DCB)))
val probe = createTestProbe[Any]()
actor ! MyReplicatedBehavior.Add("one", probe.ref)
probe.expectMessage(Done)
// simulate a published event from another replica
actor.asInstanceOf[ActorRef[Any]] ! internal.PublishedEventImpl(
ReplicationId(EntityType, id, DCB).persistenceId,
2L, // missing 1L
"two",
System.currentTimeMillis(),
Some(new ReplicatedPublishedEventMetaData(DCB, VersionVector.empty)))
actor ! MyReplicatedBehavior.Add("three", probe.ref)
probe.expectMessage(Done)
actor ! MyReplicatedBehavior.Get(probe.ref)
probe.expectMessage(Set("one", "three"))
}
"ignore a published event from an unknown replica" in {
val id = nextEntityId()
val actor = spawn(MyReplicatedBehavior(id, DCA, Set(DCA, DCB)))
val probe = createTestProbe[Any]()
actor ! MyReplicatedBehavior.Add("one", probe.ref)
probe.expectMessage(Done)
// simulate a published event from another replica
actor.asInstanceOf[ActorRef[Any]] ! internal.PublishedEventImpl(
ReplicationId(EntityType, id, DCC).persistenceId,
1L,
"two",
System.currentTimeMillis(),
Some(new ReplicatedPublishedEventMetaData(DCC, VersionVector.empty)))
actor ! MyReplicatedBehavior.Add("three", probe.ref)
probe.expectMessage(Done)
actor ! MyReplicatedBehavior.Get(probe.ref)
probe.expectMessage(Set("one", "three"))
}
"ignore an already seen event from a replica" in {
val id = nextEntityId()
val actor = spawn(MyReplicatedBehavior(id, DCA, Set(DCA, DCB)))
val probe = createTestProbe[Any]()
actor ! MyReplicatedBehavior.Add("one", probe.ref)
probe.expectMessage(Done)
// simulate a published event from another replica
actor.asInstanceOf[ActorRef[Any]] ! internal.PublishedEventImpl(
ReplicationId(EntityType, "myId4", DCB).persistenceId,
1L,
"two",
System.currentTimeMillis(),
Some(new ReplicatedPublishedEventMetaData(DCB, VersionVector.empty)))
// simulate another published event from that replica
actor.asInstanceOf[ActorRef[Any]] ! internal.PublishedEventImpl(
ReplicationId(EntityType, id, DCB).persistenceId,
1L,
"two-again", // ofc this would be the same in the real world, different just so we can detect
System.currentTimeMillis(),
Some(new ReplicatedPublishedEventMetaData(DCB, VersionVector.empty)))
actor ! MyReplicatedBehavior.Add("three", probe.ref)
probe.expectMessage(Done)
actor ! MyReplicatedBehavior.Get(probe.ref)
probe.expectMessage(Set("one", "two", "three"))
}
"handle published events after replay" in {
val id = nextEntityId()
val probe = createTestProbe[Any]()
val replicatedBehavior = MyReplicatedBehavior(id, DCA, Set(DCA, DCB))
val incarnation1 = spawn(replicatedBehavior)
incarnation1 ! MyReplicatedBehavior.Add("one", probe.ref)
probe.expectMessage(Done)
incarnation1 ! MyReplicatedBehavior.Stop
probe.expectTerminated(incarnation1)
val incarnation2 = spawn(replicatedBehavior)
incarnation2 ! MyReplicatedBehavior.Get(probe.ref)
probe.expectMessage(Set("one"))
// replay completed
// simulate a published event from another replica
incarnation2.asInstanceOf[ActorRef[Any]] ! internal.PublishedEventImpl(
ReplicationId(EntityType, id, DCB).persistenceId,
1L,
"two",
System.currentTimeMillis(),
Some(new ReplicatedPublishedEventMetaData(DCB, VersionVector.empty)))
incarnation2 ! MyReplicatedBehavior.Add("three", probe.ref)
probe.expectMessage(Done)
incarnation2 ! MyReplicatedBehavior.Get(probe.ref)
probe.expectMessage(Set("one", "two", "three"))
}
"handle published events before and after replay" in {
val id = nextEntityId()
val probe = createTestProbe[Any]()
val replicatedBehaviorA = MyReplicatedBehavior(id, DCA, Set(DCA, DCB))
val incarnationA1 = spawn(replicatedBehaviorA)
incarnationA1 ! MyReplicatedBehavior.Add("one", probe.ref)
probe.expectMessage(Done)
// simulate a published event from another replica
incarnationA1.asInstanceOf[ActorRef[Any]] ! internal.PublishedEventImpl(
ReplicationId(EntityType, id, DCB).persistenceId,
1L,
"two",
System.currentTimeMillis(),
Some(new ReplicatedPublishedEventMetaData(DCB, VersionVector.empty)))
incarnationA1 ! MyReplicatedBehavior.Stop
probe.expectTerminated(incarnationA1)
val incarnationA2 = spawn(replicatedBehaviorA)
// simulate a published event from another replica
incarnationA2.asInstanceOf[ActorRef[Any]] ! internal.PublishedEventImpl(
ReplicationId(EntityType, id, DCB).persistenceId,
2L,
"three",
System.currentTimeMillis(),
Some(new ReplicatedPublishedEventMetaData(DCB, VersionVector.empty)))
incarnationA2 ! MyReplicatedBehavior.Add("four", probe.ref)
probe.expectMessage(Done)
incarnationA2 ! MyReplicatedBehavior.Get(probe.ref)
probe.expectMessage(Set("one", "two", "three", "four"))
}
}
}

View file

@ -0,0 +1,465 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import org.apache.pekko
import pekko.Done
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.testkit.scaladsl.PersistenceTestKit
import pekko.persistence.typed.scaladsl.{ Effect, EventSourcedBehavior, ReplicatedEventSourcing, ReplicationContext }
import pekko.serialization.jackson.CborSerializable
import org.scalatest.concurrent.Eventually
import org.scalatest.wordspec.AnyWordSpecLike
object ReplicatedEventSourcingSpec {
val AllReplicas = Set(ReplicaId("R1"), ReplicaId("R2"), ReplicaId("R3"))
sealed trait Command
case class GetState(replyTo: ActorRef[State]) extends Command
case class StoreMe(description: String, replyTo: ActorRef[Done], latch: CountDownLatch = new CountDownLatch(1))
extends Command
case class StoreUs(descriptions: List[String], replyTo: ActorRef[Done], latch: CountDownLatch = new CountDownLatch(1))
extends Command
case class GetReplica(replyTo: ActorRef[(ReplicaId, Set[ReplicaId])]) extends Command
case object Stop extends Command
case class State(all: List[String]) extends CborSerializable
def testBehavior(entityId: String, replicaId: String, probe: ActorRef[EventAndContext]): Behavior[Command] =
testBehavior(entityId, replicaId, Some(probe))
def eventSourcedBehavior(
replicationContext: ReplicationContext,
probe: Option[ActorRef[EventAndContext]]): EventSourcedBehavior[Command, String, State] = {
EventSourcedBehavior[Command, String, State](
replicationContext.persistenceId,
State(Nil),
(state, command) =>
command match {
case GetState(replyTo) =>
replyTo ! state
Effect.none
case GetReplica(replyTo) =>
replyTo.tell((replicationContext.replicaId, replicationContext.allReplicas))
Effect.none
case StoreMe(evt, ack, latch) =>
latch.countDown()
latch.await(10, TimeUnit.SECONDS)
Effect.persist(evt).thenRun(_ => ack ! Done)
case StoreUs(evts, replyTo, latch) =>
latch.countDown()
latch.await(10, TimeUnit.SECONDS)
Effect.persist(evts).thenRun(_ => replyTo ! Done)
case Stop =>
Effect.stop()
},
(state, event) => {
probe.foreach(
_ ! EventAndContext(
event,
replicationContext.origin,
replicationContext.recoveryRunning,
replicationContext.concurrent))
state.copy(all = event :: state.all)
})
}
def testBehavior(
entityId: String,
replicaId: String,
probe: Option[ActorRef[EventAndContext]] = None): Behavior[Command] =
ReplicatedEventSourcing.commonJournalConfig(
ReplicationId("ReplicatedEventSourcingSpec", entityId, ReplicaId(replicaId)),
AllReplicas,
PersistenceTestKitReadJournal.Identifier)(replicationContext => eventSourcedBehavior(replicationContext, probe))
}
case class EventAndContext(event: Any, origin: ReplicaId, recoveryRunning: Boolean, concurrent: Boolean)
class ReplicatedEventSourcingSpec
extends ScalaTestWithActorTestKit(PersistenceTestKitPlugin.config)
with AnyWordSpecLike
with LogCapturing
with Eventually {
import ReplicatedEventSourcingSpec._
val ids = new AtomicInteger(0)
def nextEntityId = s"e-${ids.getAndIncrement()}"
"ReplicatedEventSourcing" should {
"replicate events between two entities" in {
val entityId = nextEntityId
val probe = createTestProbe[Done]()
val r1 = spawn(testBehavior(entityId, "R1"))
val r2 = spawn(testBehavior(entityId, "R2"))
r1 ! StoreMe("from r1", probe.ref)
r2 ! StoreMe("from r2", probe.ref)
eventually {
val probe = createTestProbe[State]()
r1 ! GetState(probe.ref)
probe.expectMessageType[State].all.toSet shouldEqual Set("from r1", "from r2")
}
eventually {
val probe = createTestProbe[State]()
r2 ! GetState(probe.ref)
probe.expectMessageType[State].all.toSet shouldEqual Set("from r1", "from r2")
}
}
"get all events in recovery" in {
val entityId = nextEntityId
val probe = createTestProbe[Done]()
val r1 = spawn(testBehavior(entityId, "R1"))
val r2 = spawn(testBehavior(entityId, "R2"))
r1 ! StoreMe("from r1", probe.ref)
r2 ! StoreMe("from r2", probe.ref)
r1 ! StoreMe("from r1 again", probe.ref)
val r3 = spawn(testBehavior(entityId, "R3"))
eventually {
val probe = createTestProbe[State]()
r3 ! GetState(probe.ref)
probe.expectMessageType[State].all.toSet shouldEqual Set("from r1", "from r2", "from r1 again")
}
}
"continue after recovery" in {
val entityId = nextEntityId
val r1Behavior = testBehavior(entityId, "R1")
val r2Behavior = testBehavior(entityId, "R2")
val probe = createTestProbe[Done]()
{
// first incarnation
val r1 = spawn(r1Behavior)
val r2 = spawn(r2Behavior)
r1 ! StoreMe("1 from r1", probe.ref)
r2 ! StoreMe("1 from r2", probe.ref)
r1 ! Stop
r2 ! Stop
probe.expectTerminated(r1)
probe.expectTerminated(r2)
}
{
// second incarnation
val r1 = spawn(r1Behavior)
val r2 = spawn(r2Behavior)
r1 ! StoreMe("2 from r1", probe.ref)
r2 ! StoreMe("2 from r2", probe.ref)
eventually {
val probe = createTestProbe[State]()
r1 ! GetState(probe.ref)
probe.expectMessageType[State].all.toSet shouldEqual Set("1 from r1", "1 from r2", "2 from r1", "2 from r2")
r2 ! GetState(probe.ref)
probe.expectMessageType[State].all.toSet shouldEqual Set("1 from r1", "1 from r2", "2 from r1", "2 from r2")
}
}
}
"have access to replica information" in {
val entityId = nextEntityId
val probe = createTestProbe[(ReplicaId, Set[ReplicaId])]()
val r1 = spawn(testBehavior(entityId, "R1"))
r1 ! GetReplica(probe.ref)
probe.expectMessage((ReplicaId("R1"), Set(ReplicaId("R1"), ReplicaId("R2"), ReplicaId("R3"))))
}
"have access to event origin" in {
val entityId = nextEntityId
val replyProbe = createTestProbe[Done]()
val eventProbeR1 = createTestProbe[EventAndContext]()
val eventProbeR2 = createTestProbe[EventAndContext]()
val r1 = spawn(testBehavior(entityId, "R1", eventProbeR1.ref))
val r2 = spawn(testBehavior(entityId, "R2", eventProbeR2.ref))
r1 ! StoreMe("from r1", replyProbe.ref)
eventProbeR2.expectMessage(EventAndContext("from r1", ReplicaId("R1"), false, false))
eventProbeR1.expectMessage(EventAndContext("from r1", ReplicaId("R1"), false, false))
r2 ! StoreMe("from r2", replyProbe.ref)
eventProbeR1.expectMessage(EventAndContext("from r2", ReplicaId("R2"), false, false))
eventProbeR2.expectMessage(EventAndContext("from r2", ReplicaId("R2"), false, false))
}
"set recovery running" in {
val entityId = nextEntityId
val eventProbeR1 = createTestProbe[EventAndContext]()
val replyProbe = createTestProbe[Done]()
val r1 = spawn(testBehavior(entityId, "R1", eventProbeR1.ref))
r1 ! StoreMe("Event", replyProbe.ref)
eventProbeR1.expectMessage(EventAndContext("Event", ReplicaId("R1"), recoveryRunning = false, false))
replyProbe.expectMessage(Done)
r1 ! Stop
replyProbe.expectTerminated(r1)
val recoveryProbe = createTestProbe[EventAndContext]()
spawn(testBehavior(entityId, "R1", recoveryProbe.ref))
recoveryProbe.expectMessage(EventAndContext("Event", ReplicaId("R1"), recoveryRunning = true, false))
}
"persist all" in {
val entityId = nextEntityId
val probe = createTestProbe[Done]()
val eventProbeR1 = createTestProbe[EventAndContext]()
val latch = new CountDownLatch(3)
val r1 = spawn(testBehavior(entityId, "R1", eventProbeR1.ref))
val r2 = spawn(testBehavior(entityId, "R2"))
r1 ! StoreUs("1 from r1" :: "2 from r1" :: Nil, probe.ref, latch)
r2 ! StoreUs("1 from r2" :: "2 from r2" :: Nil, probe.ref, latch)
// the commands have arrived in both actors, waiting for the latch,
// so that the persist of the events will be concurrent
latch.countDown()
latch.await(10, TimeUnit.SECONDS)
probe.receiveMessage()
probe.receiveMessage()
// events at r2 happened concurrently with events at r1
eventProbeR1.expectMessage(EventAndContext("1 from r1", ReplicaId("R1"), false, concurrent = false))
eventProbeR1.expectMessage(EventAndContext("2 from r1", ReplicaId("R1"), false, concurrent = false))
eventProbeR1.expectMessage(EventAndContext("1 from r2", ReplicaId("R2"), false, concurrent = true))
eventProbeR1.expectMessage(EventAndContext("2 from r2", ReplicaId("R2"), false, concurrent = true))
eventually {
val probe = createTestProbe[State]()
r1 ! GetState(probe.ref)
probe.expectMessageType[State].all.toSet shouldEqual Set("1 from r1", "2 from r1", "1 from r2", "2 from r2")
}
eventually {
val probe = createTestProbe[State]()
r2 ! GetState(probe.ref)
probe.expectMessageType[State].all.toSet shouldEqual Set("1 from r1", "2 from r1", "1 from r2", "2 from r2")
}
}
"replicate alternate events" in {
val entityId = nextEntityId
val probe = createTestProbe[Done]()
val eventProbeR1 = createTestProbe[EventAndContext]()
val eventProbeR2 = createTestProbe[EventAndContext]()
val r1 = spawn(testBehavior(entityId, "R1", eventProbeR1.ref))
val r2 = spawn(testBehavior(entityId, "R2", eventProbeR2.ref))
val latch = new CountDownLatch(3)
r1 ! StoreMe("from r1", probe.ref, latch) // R1 0 R2 0 -> R1 1 R2 0
r2 ! StoreMe("from r2", probe.ref, latch) // R2 0 R1 0 -> R2 1 R1 0
// the commands have arrived in both actors, waiting for the latch,
// so that the persist of the events will be concurrent
latch.countDown()
latch.await(10, TimeUnit.SECONDS)
latch.countDown()
// each gets its local event
eventProbeR1.expectMessage(
EventAndContext("from r1", ReplicaId("R1"), recoveryRunning = false, concurrent = false))
eventProbeR2.expectMessage(
EventAndContext("from r2", ReplicaId("R2"), recoveryRunning = false, concurrent = false))
// then the replicated remote events, which will be concurrent
eventProbeR1.expectMessage(
EventAndContext("from r2", ReplicaId("R2"), recoveryRunning = false, concurrent = true))
eventProbeR2.expectMessage(
EventAndContext("from r1", ReplicaId("R1"), recoveryRunning = false, concurrent = true))
// state is updated
eventually {
val probe = createTestProbe[State]()
r1 ! GetState(probe.ref)
probe.expectMessageType[State].all.toSet shouldEqual Set("from r1", "from r2")
}
eventually {
val probe = createTestProbe[State]()
r2 ! GetState(probe.ref)
probe.expectMessageType[State].all.toSet shouldEqual Set("from r1", "from r2")
}
// Neither of these should be concurrent, nothing happening at r2
r1 ! StoreMe("from r1 2", probe.ref) // R1 1 R2 1
eventProbeR1.expectMessage(EventAndContext("from r1 2", ReplicaId("R1"), false, concurrent = false))
eventProbeR2.expectMessage(EventAndContext("from r1 2", ReplicaId("R1"), false, concurrent = false))
r1 ! StoreMe("from r1 3", probe.ref) // R2 2 R2 1
eventProbeR1.expectMessage(EventAndContext("from r1 3", ReplicaId("R1"), false, concurrent = false))
eventProbeR2.expectMessage(EventAndContext("from r1 3", ReplicaId("R1"), false, concurrent = false))
eventually {
val probe = createTestProbe[State]()
r2 ! GetState(probe.ref)
probe.expectMessageType[State].all.toSet shouldEqual Set("from r1", "from r2", "from r1 2", "from r1 3")
}
// not concurrent as the above asserts mean that all events are fully replicated
r2 ! StoreMe("from r2 2", probe.ref)
eventProbeR1.expectMessage(EventAndContext("from r2 2", ReplicaId("R2"), false, concurrent = false))
eventProbeR2.expectMessage(EventAndContext("from r2 2", ReplicaId("R2"), false, concurrent = false))
eventually {
val probe = createTestProbe[State]()
r1 ! GetState(probe.ref)
probe.expectMessageType[State].all.toSet shouldEqual Set(
"from r1",
"from r2",
"from r1 2",
"from r1 3",
"from r2 2")
}
}
"receive each event only once" in {
val entityId = nextEntityId
val probe = createTestProbe[Done]()
val eventProbeR1 = createTestProbe[EventAndContext]()
val eventProbeR2 = createTestProbe[EventAndContext]()
val r1 = spawn(testBehavior(entityId, "R1", eventProbeR1.ref))
val r2 = spawn(testBehavior(entityId, "R2", eventProbeR2.ref))
r1 ! StoreMe("from r1 1", probe.ref)
probe.expectMessage(Done)
r1 ! StoreMe("from r1 2", probe.ref)
probe.expectMessage(Done)
// r2, in order because we wrote them both in r1
eventProbeR2.expectMessage(EventAndContext("from r1 1", ReplicaId("R1"), false, false))
eventProbeR2.expectMessage(EventAndContext("from r1 2", ReplicaId("R1"), false, false))
r2 ! StoreMe("from r2 1", probe.ref)
probe.expectMessage(Done)
r2 ! StoreMe("from r2 2", probe.ref)
probe.expectMessage(Done)
// r3 should only get the events 1, not R2s stored version of them, but we don't know the
// order they will arrive
val eventProbeR3 = createTestProbe[EventAndContext]()
spawn(testBehavior(entityId, "R3", eventProbeR3.ref))
val eventAndContexts = eventProbeR3.receiveMessages(4).toSet
eventAndContexts should ===(
Set(
EventAndContext("from r1 1", ReplicaId("R1"), false, false),
EventAndContext("from r1 2", ReplicaId("R1"), false, false),
EventAndContext("from r2 1", ReplicaId("R2"), false, false),
EventAndContext("from r2 2", ReplicaId("R2"), false, false)))
eventProbeR3.expectNoMessage()
}
"set concurrent on replay of events" in {
val entityId = nextEntityId
val probe = createTestProbe[Done]()
val eventProbeR1 = createTestProbe[EventAndContext]()
val r1 = spawn(testBehavior(entityId, "R1", eventProbeR1.ref))
val r2 = spawn(testBehavior(entityId, "R2"))
val latch = new CountDownLatch(3)
r1 ! StoreMe("from r1", probe.ref, latch) // R1 0 R2 0 -> R1 1 R2 0
r2 ! StoreMe("from r2", probe.ref, latch) // R2 0 R1 0 -> R2 1 R1 0
// the commands have arrived in both actors, waiting for the latch,
// so that the persist of the events will be concurrent
latch.countDown()
latch.await(10, TimeUnit.SECONDS)
// local event isn't concurrent, remote event is
eventProbeR1.expectMessage(
EventAndContext("from r1", ReplicaId("R1"), recoveryRunning = false, concurrent = false))
eventProbeR1.expectMessage(
EventAndContext("from r2", ReplicaId("R2"), recoveryRunning = false, concurrent = true))
r1 ! Stop
r2 ! Stop
probe.expectTerminated(r1)
probe.expectTerminated(r2)
// take 2
val eventProbeR1Take2 = createTestProbe[EventAndContext]()
spawn(testBehavior(entityId, "R1", eventProbeR1Take2.ref))
eventProbeR1Take2.expectMessage(
EventAndContext("from r1", ReplicaId("R1"), recoveryRunning = true, concurrent = false))
eventProbeR1Take2.expectMessage(
EventAndContext("from r2", ReplicaId("R2"), recoveryRunning = true, concurrent = true))
}
"replicate events between three entities" in {
val entityId = nextEntityId
val probe = createTestProbe[Done]()
var r1 = spawn(testBehavior(entityId, "R1"))
var r2 = spawn(testBehavior(entityId, "R2"))
var r3 = spawn(testBehavior(entityId, "R3"))
r1 ! StoreMe("1 from r1", probe.ref)
r2 ! StoreMe("1 from r2", probe.ref)
r3 ! StoreMe("1 from r3", probe.ref)
probe.receiveMessages(3) // all writes acked
(r1 :: r2 :: r3 :: Nil).foreach { replica =>
eventually {
val probe = createTestProbe[State]()
replica ! GetState(probe.ref)
probe.expectMessageType[State].all.toSet shouldEqual Set("1 from r1", "1 from r2", "1 from r3")
replica ! Stop
probe.expectTerminated(replica)
}
}
// with all replicas stopped, start and write a bit to one of them
r1 = spawn(testBehavior(entityId, "R1"))
r1 ! StoreMe("2 from r1", probe.ref)
r1 ! StoreMe("3 from r1", probe.ref)
probe.receiveMessages(2) // both writes acked
r1 ! Stop
probe.expectTerminated(r1)
// start the other two
r1 = spawn(testBehavior(entityId, "R1"))
r2 = spawn(testBehavior(entityId, "R2"))
r3 = spawn(testBehavior(entityId, "R3"))
(r1 :: r2 :: r3 :: Nil).foreach { replica =>
eventually {
val probe = createTestProbe[State]()
replica ! GetState(probe.ref)
probe.expectMessageType[State].all.toSet shouldEqual Set(
"1 from r1",
"2 from r1",
"3 from r1",
"1 from r2",
"1 from r3")
replica ! Stop
probe.expectTerminated(replica)
}
}
}
"restart replication stream" in {
val testkit = PersistenceTestKit(system)
val entityId = nextEntityId
val stateProbe = createTestProbe[State]()
val probe = createTestProbe[Done]()
val eventProbeR1 = createTestProbe[EventAndContext]()
val r1 = spawn(testBehavior(entityId, "R1", eventProbeR1.ref))
val r2 = spawn(testBehavior(entityId, "R2"))
// ensure recovery is complete
r1 ! GetState(stateProbe.ref)
stateProbe.expectMessage(State(Nil))
r2 ! GetState(stateProbe.ref)
stateProbe.expectMessage(State(Nil))
// make reads fail for the replication
testkit.failNextNReads(s"$entityId|R2", 1)
// should restart the replication stream
r2 ! StoreMe("from r2", probe.ref)
eventProbeR1.expectMessageType[EventAndContext].event shouldEqual "from r2"
}
}
}

View file

@ -0,0 +1,116 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed
import java.util.concurrent.atomic.AtomicInteger
import org.apache.pekko
import pekko.Done
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.typed.ActorRef
import pekko.persistence.query.NoOffset
import pekko.persistence.query.scaladsl.CurrentEventsByTagQuery
import pekko.persistence.query.PersistenceQuery
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.scaladsl.Effect
import pekko.persistence.typed.scaladsl.EventSourcedBehavior
import pekko.persistence.typed.scaladsl.ReplicatedEventSourcing
import pekko.stream.scaladsl.Sink
import pekko.serialization.jackson.CborSerializable
import org.scalatest.concurrent.Eventually
import org.scalatest.wordspec.AnyWordSpecLike
object ReplicatedEventSourcingTaggingSpec {
val ReplicaId1 = ReplicaId("R1")
val ReplicaId2 = ReplicaId("R2")
val AllReplicas = Set(ReplicaId1, ReplicaId2)
val queryPluginId = PersistenceTestKitReadJournal.Identifier
object ReplicatedStringSet {
sealed trait Command
case class Add(description: String, replyTo: ActorRef[Done]) extends Command
case class GetStrings(replyTo: ActorRef[Set[String]]) extends Command
case class State(strings: Set[String]) extends CborSerializable
def apply(
entityId: String,
replica: ReplicaId,
allReplicas: Set[ReplicaId]): EventSourcedBehavior[Command, String, State] = {
// #tagging
ReplicatedEventSourcing.commonJournalConfig(
ReplicationId("TaggingSpec", entityId, replica),
allReplicas,
queryPluginId)(replicationContext =>
EventSourcedBehavior[Command, String, State](
replicationContext.persistenceId,
State(Set.empty),
(state, command) =>
command match {
case Add(string, ack) =>
if (state.strings.contains(string)) Effect.none.thenRun(_ => ack ! Done)
else Effect.persist(string).thenRun(_ => ack ! Done)
case GetStrings(replyTo) =>
replyTo ! state.strings
Effect.none
},
(state, event) => state.copy(strings = state.strings + event))
// use withTagger to define tagging logic
.withTagger(event =>
// don't apply tags if event was replicated here, it already will appear in queries by tag
// as the origin replica would have tagged it already
if (replicationContext.origin != replicationContext.replicaId) Set.empty
else if (event.length > 10) Set("long-strings", "strings")
else Set("strings")))
// #tagging
}
}
}
class ReplicatedEventSourcingTaggingSpec
extends ScalaTestWithActorTestKit(PersistenceTestKitPlugin.config)
with AnyWordSpecLike
with LogCapturing
with Eventually {
import ReplicatedEventSourcingTaggingSpec._
val ids = new AtomicInteger(0)
def nextEntityId = s"e-${ids.getAndIncrement()}"
"ReplicatedEventSourcing" should {
"allow for tagging of events using the replication context" in {
val entityId = nextEntityId
val probe = createTestProbe[Done]()
val r1 = spawn(ReplicatedStringSet(entityId, ReplicaId1, AllReplicas))
val r2 = spawn(ReplicatedStringSet(entityId, ReplicaId2, AllReplicas))
r1 ! ReplicatedStringSet.Add("from r1", probe.ref)
r2 ! ReplicatedStringSet.Add("from r2", probe.ref)
probe.receiveMessages(2)
r1 ! ReplicatedStringSet.Add("a very long string from r1", probe.ref)
probe.receiveMessages(1)
val allEvents = Set("from r1", "from r2", "a very long string from r1")
for (replica <- r1 :: r2 :: Nil) {
eventually {
val probe = testKit.createTestProbe[Set[String]]()
replica ! ReplicatedStringSet.GetStrings(probe.ref)
probe.receiveMessage() should ===(allEvents)
}
}
val query =
PersistenceQuery(system).readJournalFor[CurrentEventsByTagQuery](PersistenceTestKitReadJournal.Identifier)
val stringTaggedEvents = query.currentEventsByTag("strings", NoOffset).runWith(Sink.seq).futureValue
stringTaggedEvents.map(_.event).toSet should equal(allEvents)
val longStrings = query.currentEventsByTag("long-strings", NoOffset).runWith(Sink.seq).futureValue
longStrings should have size 1
}
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed
import java.util.concurrent.atomic.AtomicInteger
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.{ LogCapturing, ScalaTestWithActorTestKit }
import pekko.persistence.testkit.{ PersistenceTestKitPlugin, PersistenceTestKitSnapshotPlugin }
import org.scalatest.concurrent.Eventually
import org.scalatest.wordspec.AnyWordSpecLike
object ReplicationBaseSpec {
val R1 = ReplicaId("R1")
val R2 = ReplicaId("R2")
val AllReplicas = Set(R1, R2)
}
abstract class ReplicationBaseSpec
extends ScalaTestWithActorTestKit(
PersistenceTestKitPlugin.config.withFallback(PersistenceTestKitSnapshotPlugin.config))
with AnyWordSpecLike
with LogCapturing
with Eventually {
val ids = new AtomicInteger(0)
def nextEntityId: String = s"e-${ids.getAndIncrement()}"
}

View file

@ -0,0 +1,104 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.{ LogCapturing, ScalaTestWithActorTestKit }
import pekko.actor.typed.{ ActorRef, Behavior }
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.scaladsl.{ Effect, EventSourcedBehavior, ReplicatedEventSourcing }
import pekko.serialization.jackson.CborSerializable
import org.scalatest.concurrent.Eventually
import org.scalatest.wordspec.AnyWordSpecLike
object ReplicationIllegalAccessSpec {
val R1 = ReplicaId("R1")
val R2 = ReplicaId("R1")
val AllReplicas = Set(R1, R2)
sealed trait Command
case class AccessInCommandHandler(replyTo: ActorRef[Thrown]) extends Command
case class AccessInPersistCallback(replyTo: ActorRef[Thrown]) extends Command
case class Thrown(exception: Option[Throwable])
case class State(all: List[String]) extends CborSerializable
def apply(entityId: String, replica: ReplicaId): Behavior[Command] = {
ReplicatedEventSourcing.commonJournalConfig(
ReplicationId("IllegalAccessSpec", entityId, replica),
AllReplicas,
PersistenceTestKitReadJournal.Identifier)(replicationContext =>
EventSourcedBehavior[Command, String, State](
replicationContext.persistenceId,
State(Nil),
(_, command) =>
command match {
case AccessInCommandHandler(replyTo) =>
val exception =
try {
replicationContext.origin
None
} catch {
case t: Throwable =>
Some(t)
}
replyTo ! Thrown(exception)
Effect.none
case AccessInPersistCallback(replyTo) =>
Effect.persist("cat").thenRun { _ =>
val exception =
try {
replicationContext.concurrent
None
} catch {
case t: Throwable =>
Some(t)
}
replyTo ! Thrown(exception)
}
},
(state, event) => state.copy(all = event :: state.all)))
}
}
class ReplicationIllegalAccessSpec
extends ScalaTestWithActorTestKit(PersistenceTestKitPlugin.config)
with AnyWordSpecLike
with LogCapturing
with Eventually {
import ReplicationIllegalAccessSpec._
"ReplicatedEventSourcing" should {
"detect illegal access to context in command handler" in {
val probe = createTestProbe[Thrown]()
val ref = spawn(ReplicationIllegalAccessSpec("id1", R1))
ref ! AccessInCommandHandler(probe.ref)
val thrown: Throwable = probe.expectMessageType[Thrown].exception.get
thrown.getMessage should include("from the event handler")
}
"detect illegal access to context in persist thenRun" in {
val probe = createTestProbe[Thrown]()
val ref = spawn(ReplicationIllegalAccessSpec("id1", R1))
ref ! AccessInPersistCallback(probe.ref)
val thrown: Throwable = probe.expectMessageType[Thrown].exception.get
thrown.getMessage should include("from the event handler")
}
"detect illegal access in the factory" in {
val exception = intercept[UnsupportedOperationException] {
ReplicatedEventSourcing.commonJournalConfig(
ReplicationId("IllegalAccessSpec", "id2", R1),
AllReplicas,
PersistenceTestKitReadJournal.Identifier) { replicationContext =>
replicationContext.origin
???
}
}
exception.getMessage should include("from the event handler")
}
}
}

View file

@ -0,0 +1,113 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed
import java.util.concurrent.atomic.AtomicInteger
import org.apache.pekko
import pekko.Done
import pekko.actor.testkit.typed.scaladsl.{ LogCapturing, ScalaTestWithActorTestKit }
import pekko.actor.typed.{ ActorRef, Behavior }
import pekko.persistence.testkit.{ PersistenceTestKitPlugin, PersistenceTestKitSnapshotPlugin }
import pekko.persistence.testkit.scaladsl.{ PersistenceTestKit, SnapshotTestKit }
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.internal.{ ReplicatedPublishedEventMetaData, VersionVector }
import pekko.persistence.typed.scaladsl.ReplicatedEventSourcing
import org.scalatest.concurrent.Eventually
import org.scalatest.wordspec.AnyWordSpecLike
object ReplicationSnapshotSpec {
import ReplicatedEventSourcingSpec._
val EntityType = "SnapshotSpec"
def behaviorWithSnapshotting(entityId: String, replicaId: ReplicaId): Behavior[Command] =
behaviorWithSnapshotting(entityId, replicaId, None)
def behaviorWithSnapshotting(
entityId: String,
replicaId: ReplicaId,
eventProbe: ActorRef[EventAndContext]): Behavior[Command] =
behaviorWithSnapshotting(entityId, replicaId, Some(eventProbe))
def behaviorWithSnapshotting(
entityId: String,
replicaId: ReplicaId,
probe: Option[ActorRef[EventAndContext]]): Behavior[Command] = {
ReplicatedEventSourcing.commonJournalConfig(
ReplicationId(EntityType, entityId, replicaId),
AllReplicas,
PersistenceTestKitReadJournal.Identifier)(replicationContext =>
eventSourcedBehavior(replicationContext, probe).snapshotWhen((_, _, sequenceNr) => sequenceNr % 2 == 0))
}
}
class ReplicationSnapshotSpec
extends ScalaTestWithActorTestKit(
PersistenceTestKitPlugin.config.withFallback(PersistenceTestKitSnapshotPlugin.config))
with AnyWordSpecLike
with LogCapturing
with Eventually {
import ReplicatedEventSourcingSpec._
import ReplicationSnapshotSpec._
val ids = new AtomicInteger(0)
def nextEntityId = s"e-${ids.getAndIncrement()}"
val snapshotTestKit = SnapshotTestKit(system)
val persistenceTestKit = PersistenceTestKit(system)
val R1 = ReplicaId("R1")
val R2 = ReplicaId("R2")
"ReplicatedEventSourcing" should {
"recover state from snapshots" in {
val entityId = nextEntityId
val persistenceIdR1 = s"$EntityType|$entityId|R1"
val persistenceIdR2 = s"$EntityType|$entityId|R2"
val probe = createTestProbe[Done]()
val r2EventProbe = createTestProbe[EventAndContext]()
{
val r1 = spawn(behaviorWithSnapshotting(entityId, R1))
val r2 = spawn(behaviorWithSnapshotting(entityId, R2, r2EventProbe.ref))
r1 ! StoreMe("r1 1", probe.ref)
r1 ! StoreMe("r1 2", probe.ref)
r2EventProbe.expectMessageType[EventAndContext]
r2EventProbe.expectMessageType[EventAndContext]
snapshotTestKit.expectNextPersisted(persistenceIdR1, State(List("r1 2", "r1 1")))
snapshotTestKit.expectNextPersisted(persistenceIdR2, State(List("r1 2", "r1 1")))
r2.asInstanceOf[ActorRef[Any]] ! internal.PublishedEventImpl(
ReplicationId(EntityType, entityId, R1).persistenceId,
1L,
"two-again",
System.currentTimeMillis(),
Some(new ReplicatedPublishedEventMetaData(R1, VersionVector.empty)))
// r2 should now filter out that event if it receives it again
r2EventProbe.expectNoMessage()
}
// restart r2 from a snapshot, the event should still be filtered
{
val r2 = spawn(behaviorWithSnapshotting(entityId, R2, r2EventProbe.ref))
r2.asInstanceOf[ActorRef[Any]] ! internal.PublishedEventImpl(
ReplicationId(EntityType, entityId, R1).persistenceId,
1L,
"two-again",
System.currentTimeMillis(),
Some(new ReplicatedPublishedEventMetaData(R1, VersionVector.empty)))
r2EventProbe.expectNoMessage()
val stateProbe = createTestProbe[State]()
r2 ! GetState(stateProbe.ref)
stateProbe.expectMessage(State(List("r1 2", "r1 1")))
}
}
}
}

View file

@ -0,0 +1,133 @@
/*
* Copyright (C) 2017-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.crdt
import org.apache.pekko
import pekko.actor.typed.ActorRef
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.ReplicationId
import pekko.persistence.typed.crdt.CounterSpec.PlainCounter.{ Decrement, Get, Increment }
import pekko.persistence.typed.scaladsl.{ Effect, EventSourcedBehavior, ReplicatedEventSourcing }
import pekko.persistence.typed.{ ReplicaId, ReplicationBaseSpec }
object CounterSpec {
object PlainCounter {
sealed trait Command
case class Get(reply: ActorRef[Long]) extends Command
case object Increment extends Command
case object Decrement extends Command
}
import ReplicationBaseSpec._
def apply(
entityId: String,
replicaId: ReplicaId,
snapshotEvery: Long = 100,
eventProbe: Option[ActorRef[Counter.Updated]] = None) =
Behaviors.setup[PlainCounter.Command] { context =>
ReplicatedEventSourcing.commonJournalConfig(
ReplicationId("CounterSpec", entityId, replicaId),
AllReplicas,
PersistenceTestKitReadJournal.Identifier) { ctx =>
EventSourcedBehavior[PlainCounter.Command, Counter.Updated, Counter](
ctx.persistenceId,
Counter.empty,
(state, command) =>
command match {
case PlainCounter.Increment =>
context.log.info("Increment. Current state {}", state.value)
Effect.persist(Counter.Updated(1))
case PlainCounter.Decrement =>
Effect.persist(Counter.Updated(-1))
case Get(replyTo) =>
context.log.info("Get request. {} {}", state.value, state.value.longValue)
replyTo ! state.value.longValue
Effect.none
},
(counter, event) => {
eventProbe.foreach(_ ! event)
counter.applyOperation(event)
}).snapshotWhen { (_, _, seqNr) =>
seqNr % snapshotEvery == 0
}
}
}
}
class CounterSpec extends ReplicationBaseSpec {
import CounterSpec._
import ReplicationBaseSpec._
"Replicated entity using CRDT counter" should {
"replicate" in {
val id = nextEntityId
val r1 = spawn(apply(id, R1))
val r2 = spawn(apply(id, R2))
val r1Probe = createTestProbe[Long]()
val r2Probe = createTestProbe[Long]()
r1 ! Increment
r1 ! Increment
eventually {
r1 ! Get(r1Probe.ref)
r1Probe.expectMessage(2L)
r2 ! Get(r2Probe.ref)
r2Probe.expectMessage(2L)
}
for (n <- 1 to 10) {
if (n % 2 == 0) r1 ! Increment
else r1 ! Decrement
}
for (_ <- 1 to 10) {
r2 ! Increment
}
eventually {
r1 ! Get(r1Probe.ref)
r1Probe.expectMessage(12L)
r2 ! Get(r2Probe.ref)
r2Probe.expectMessage(12L)
}
}
}
"recover from snapshot" in {
val id = nextEntityId
{
val r1 = spawn(apply(id, R1, 2))
val r2 = spawn(apply(id, R2, 2))
val r1Probe = createTestProbe[Long]()
val r2Probe = createTestProbe[Long]()
r1 ! Increment
r1 ! Increment
eventually {
r1 ! Get(r1Probe.ref)
r1Probe.expectMessage(2L)
r2 ! Get(r2Probe.ref)
r2Probe.expectMessage(2L)
}
}
{
val r2EventProbe = createTestProbe[Counter.Updated]()
val r2 = spawn(apply(id, R2, 2, Some(r2EventProbe.ref)))
val r2Probe = createTestProbe[Long]()
eventually {
r2 ! Get(r2Probe.ref)
r2Probe.expectMessage(2L)
}
r2EventProbe.expectNoMessage()
}
}
}

View file

@ -0,0 +1,130 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.crdt
import org.apache.pekko
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.ReplicationId
import pekko.persistence.typed.scaladsl.Effect
import pekko.persistence.typed.scaladsl.EventSourcedBehavior
import pekko.persistence.typed.scaladsl.ReplicatedEventSourcing
import pekko.persistence.typed.ReplicaId
import pekko.persistence.typed.ReplicationBaseSpec
import pekko.serialization.jackson.CborSerializable
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
object LwwSpec {
import ReplicationBaseSpec._
sealed trait Command
final case class Update(item: String, timestamp: Long, error: ActorRef[String], latch: Option[CountDownLatch])
extends Command
final case class Get(replyTo: ActorRef[Registry]) extends Command
sealed trait Event extends CborSerializable
final case class Changed(item: String, timestamp: LwwTime) extends Event
final case class Registry(item: String, updatedTimestamp: LwwTime) extends CborSerializable
object LwwRegistry {
def apply(entityId: String, replica: ReplicaId): Behavior[Command] = {
ReplicatedEventSourcing.commonJournalConfig(
ReplicationId("LwwRegistrySpec", entityId, replica),
AllReplicas,
PersistenceTestKitReadJournal.Identifier) { replicationContext =>
EventSourcedBehavior[Command, Event, Registry](
replicationContext.persistenceId,
Registry("", LwwTime(Long.MinValue, replicationContext.replicaId)),
(state, command) =>
command match {
case Update(s, timestmap, error, maybeLatch) =>
if (s == "") {
error ! "bad value"
Effect.none
} else {
maybeLatch.foreach { l =>
l.countDown()
l.await(10, TimeUnit.SECONDS)
}
Effect.persist(Changed(s, state.updatedTimestamp.increase(timestmap, replicationContext.replicaId)))
}
case Get(replyTo) =>
replyTo ! state
Effect.none
},
(state, event) =>
event match {
case Changed(s, timestamp) =>
if (timestamp.isAfter(state.updatedTimestamp)) Registry(s, timestamp)
else state
})
}
}
}
}
class LwwSpec extends ReplicationBaseSpec {
import LwwSpec._
import ReplicationBaseSpec._
class Setup {
val entityId = nextEntityId
val r1 = spawn(LwwRegistry.apply(entityId, R1))
val r2 = spawn(LwwRegistry.apply(entityId, R2))
val r1Probe = createTestProbe[String]()
val r2Probe = createTestProbe[String]()
val r1GetProbe = createTestProbe[Registry]()
val r2GetProbe = createTestProbe[Registry]()
}
"Lww Replicated Event Sourced Behavior" should {
"replicate a single event" in new Setup {
r1 ! Update("a1", 1L, r1Probe.ref, None)
eventually {
val probe = createTestProbe[Registry]()
r2 ! Get(probe.ref)
probe.expectMessage(Registry("a1", LwwTime(1L, R1)))
}
}
"resolve conflict" in new Setup {
r1 ! Update("a1", 1L, r1Probe.ref, None)
r2 ! Update("b1", 2L, r2Probe.ref, None)
eventually {
r1 ! Get(r1GetProbe.ref)
r2 ! Get(r2GetProbe.ref)
r1GetProbe.expectMessage(Registry("b1", LwwTime(2L, R2)))
r2GetProbe.expectMessage(Registry("b1", LwwTime(2L, R2)))
}
}
"have deterministic tiebreak when the same time" in new Setup {
val latch = new CountDownLatch(3)
r1 ! Update("a1", 1L, r1Probe.ref, Some(latch))
r2 ! Update("b1", 1L, r2Probe.ref, Some(latch))
// the commands have arrived in both actors, waiting for the latch,
// so that the persist of the events will be concurrent
latch.countDown()
latch.await(10, TimeUnit.SECONDS)
// R1 < R2
eventually {
r1 ! Get(r1GetProbe.ref)
r2 ! Get(r2GetProbe.ref)
r1GetProbe.expectMessage(Registry("a1", LwwTime(1L, R1)))
r2GetProbe.expectMessage(Registry("a1", LwwTime(1L, R1)))
}
}
}
}

View file

@ -0,0 +1,103 @@
/*
* Copyright (C) 2017-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.crdt
import org.apache.pekko
import pekko.actor.typed.{ ActorRef, Behavior }
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.scaladsl.{ Effect, EventSourcedBehavior, ReplicatedEventSourcing }
import pekko.persistence.typed.{ ReplicaId, ReplicationBaseSpec }
import ORSetSpec.ORSetEntity._
import pekko.persistence.typed.ReplicationBaseSpec.{ R1, R2 }
import pekko.persistence.typed.ReplicationId
import pekko.persistence.typed.crdt.ORSetSpec.ORSetEntity
import scala.util.Random
object ORSetSpec {
import ReplicationBaseSpec._
object ORSetEntity {
sealed trait Command
final case class Get(replyTo: ActorRef[Set[String]]) extends Command
final case class Add(elem: String) extends Command
final case class AddAll(elems: Set[String]) extends Command
final case class Remove(elem: String) extends Command
def apply(entityId: String, replica: ReplicaId): Behavior[ORSetEntity.Command] = {
ReplicatedEventSourcing.commonJournalConfig(
ReplicationId("ORSetSpec", entityId, replica),
AllReplicas,
PersistenceTestKitReadJournal.Identifier) { replicationContext =>
EventSourcedBehavior[Command, ORSet.DeltaOp, ORSet[String]](
replicationContext.persistenceId,
ORSet(replica),
(state, command) =>
command match {
case Add(elem) =>
Effect.persist(state + elem)
case AddAll(elems) =>
Effect.persist(state.addAll(elems.toSet))
case Remove(elem) =>
Effect.persist(state - elem)
case Get(replyTo) =>
Effect.none.thenRun(state => replyTo ! state.elements)
},
(state, operation) => state.applyOperation(operation))
}
}
}
}
class ORSetSpec extends ReplicationBaseSpec {
class Setup {
val entityId = nextEntityId
val r1 = spawn(ORSetEntity.apply(entityId, R1))
val r2 = spawn(ORSetEntity.apply(entityId, R2))
val r1GetProbe = createTestProbe[Set[String]]()
val r2GetProbe = createTestProbe[Set[String]]()
def assertForAllReplicas(state: Set[String]): Unit = {
eventually {
r1 ! Get(r1GetProbe.ref)
r1GetProbe.expectMessage(state)
r2 ! Get(r2GetProbe.ref)
r2GetProbe.expectMessage(state)
}
}
}
def randomDelay(): Unit = {
// exercise different timing scenarios
Thread.sleep(Random.nextInt(200).toLong)
}
"ORSet Replicated Entity" should {
"support concurrent updates" in new Setup {
r1 ! Add("a1")
r2 ! Add("b1")
assertForAllReplicas(Set("a1", "b1"))
r2 ! Remove("b1")
assertForAllReplicas(Set("a1"))
r2 ! Add("b1")
for (n <- 2 to 10) {
r1 ! Add(s"a$n")
if (n % 3 == 0)
randomDelay()
r2 ! Add(s"b$n")
}
r1 ! AddAll((11 to 13).map(n => s"a$n").toSet)
r2 ! AddAll((11 to 13).map(n => s"b$n").toSet)
val expected = (1 to 13).flatMap(n => List(s"a$n", s"b$n")).toSet
assertForAllReplicas(expected)
}
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.jackson
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.{ LogCapturing, ScalaTestWithActorTestKit, SerializationTestKit }
import pekko.persistence.typed.ReplicaId
import pekko.persistence.typed.crdt.{ Counter, LwwTime, ORSet }
import pekko.persistence.typed.jackson.ReplicatedEventSourcingJacksonSpec.{ WithCounter, WithLwwTime, WithOrSet }
import pekko.serialization.jackson.{ JsonSerializable, PekkoSerializationDeserializer, PekkoSerializationSerializer }
import com.fasterxml.jackson.databind.annotation.{ JsonDeserialize, JsonSerialize }
import org.scalatest.wordspec.AnyWordSpecLike
object ReplicatedEventSourcingJacksonSpec {
final case class WithLwwTime(lwwTime: LwwTime) extends JsonSerializable
final case class WithOrSet(
@JsonDeserialize(`using` = classOf[PekkoSerializationDeserializer])
@JsonSerialize(`using` = classOf[PekkoSerializationSerializer])
orSet: ORSet[String])
extends JsonSerializable
final case class WithCounter(
@JsonDeserialize(`using` = classOf[PekkoSerializationDeserializer])
@JsonSerialize(`using` = classOf[PekkoSerializationSerializer])
counter: Counter)
extends JsonSerializable
}
class ReplicatedEventSourcingJacksonSpec extends ScalaTestWithActorTestKit with AnyWordSpecLike with LogCapturing {
private val serializationTestkit = new SerializationTestKit(system)
"RES jackson" should {
"serialize LwwTime" in {
val obj = WithLwwTime(LwwTime(5, ReplicaId("A")))
serializationTestkit.verifySerialization(obj)
}
"serialize ORSet" in {
val emptyOrSet = WithOrSet(ORSet.empty[String](ReplicaId("A")))
serializationTestkit.verifySerialization(emptyOrSet)
}
"serialize Counter" in {
val counter = WithCounter(Counter.empty)
serializationTestkit.verifySerialization(counter)
}
}
}

View file

@ -0,0 +1,325 @@
/*
* Copyright (C) 2017-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.TestException
import pekko.actor.testkit.typed.TestKitSettings
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.PostStop
import pekko.actor.typed.PreRestart
import pekko.actor.typed.Signal
import pekko.actor.typed.SupervisorStrategy
import pekko.actor.typed.scaladsl.Behaviors
import pekko.actor.typed.scaladsl.adapter._
import pekko.persistence.AtomicWrite
import pekko.persistence.journal.inmem.InmemJournal
import pekko.persistence.typed.EventRejectedException
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.RecoveryCompleted
import pekko.persistence.typed.RecoveryFailed
import pekko.persistence.typed.internal.JournalFailureException
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
import scala.collection.immutable
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.Try
class ChaosJournal extends InmemJournal {
var counts = Map.empty[String, Int]
var failRecovery = true
var reject = true
override def asyncWriteMessages(messages: immutable.Seq[AtomicWrite]): Future[immutable.Seq[Try[Unit]]] = {
val pid = messages.head.persistenceId
counts = counts.updated(pid, counts.getOrElse(pid, 0) + 1)
if (pid == "fail-first-2" && counts(pid) <= 2) {
Future.failed(TestException("database says no"))
} else if (pid.startsWith("fail-fifth") && counts(pid) == 5) {
Future.failed(TestException("database says no"))
} else if (pid == "reject-first" && reject) {
reject = false
Future.successful(messages.map(_ =>
Try {
throw TestException("I don't like it")
}))
} else if (messages.head.payload.head.payload == "malicious") {
super.asyncWriteMessages(List(AtomicWrite(List(messages.head.payload.head.withPayload("wrong-event")))))
} else {
super.asyncWriteMessages(messages)
}
}
override def asyncReadHighestSequenceNr(persistenceId: String, fromSequenceNr: Long): Future[Long] = {
if (persistenceId == "fail-recovery-once" && failRecovery) {
failRecovery = false
Future.failed(TestException("Nah"))
} else if (persistenceId == "fail-recovery") {
Future.failed(TestException("Nope"))
} else {
super.asyncReadHighestSequenceNr(persistenceId, fromSequenceNr)
}
}
}
object EventSourcedBehaviorFailureSpec {
val conf: Config = ConfigFactory.parseString(s"""
pekko.loglevel = INFO
pekko.persistence.journal.plugin = "failure-journal"
failure-journal = $${pekko.persistence.journal.inmem}
failure-journal {
class = "org.apache.pekko.persistence.typed.scaladsl.ChaosJournal"
}
""").withFallback(ConfigFactory.defaultReference()).resolve()
}
class EventSourcedBehaviorFailureSpec
extends ScalaTestWithActorTestKit(EventSourcedBehaviorFailureSpec.conf)
with AnyWordSpecLike
with LogCapturing {
implicit val testSettings: TestKitSettings = TestKitSettings(system)
def failingPersistentActor(
pid: PersistenceId,
probe: ActorRef[String],
additionalSignalHandler: PartialFunction[(String, Signal), Unit] = PartialFunction.empty)
: EventSourcedBehavior[String, String, String] =
EventSourcedBehavior[String, String, String](
pid,
"",
(_, cmd) => {
if (cmd == "wrong")
throw TestException("wrong command")
probe.tell("persisting")
Effect.persist(cmd).thenRun { _ =>
probe.tell("persisted")
if (cmd == "wrong-callback") throw TestException("wrong command")
}
},
(state, event) => {
if (event == "wrong-event")
throw TestException("wrong event")
probe.tell(event)
state + event
}).receiveSignal(additionalSignalHandler.orElse {
case (_, RecoveryCompleted) =>
probe.tell("starting")
case (_, PostStop) =>
probe.tell("stopped")
case (_, PreRestart) =>
probe.tell("restarting")
})
"A typed persistent actor (failures)" must {
"signal RecoveryFailure when replay fails" in {
LoggingTestKit.error[JournalFailureException].expect {
val probe = TestProbe[String]()
val excProbe = TestProbe[Throwable]()
spawn(failingPersistentActor(PersistenceId.ofUniqueId("fail-recovery"), probe.ref,
{
case (_, RecoveryFailed(t)) =>
excProbe.ref ! t
}))
excProbe.expectMessageType[TestException].message shouldEqual "Nope"
probe.expectMessage("stopped")
}
}
"handle exceptions from RecoveryFailed signal handler" in {
val probe = TestProbe[String]()
val pa = spawn(failingPersistentActor(PersistenceId.ofUniqueId("fail-recovery-twice"), probe.ref,
{
case (_, RecoveryFailed(_)) =>
throw TestException("recovery call back failure")
}))
pa ! "one"
probe.expectMessage("starting")
probe.expectMessage("persisting")
probe.expectMessage("one")
probe.expectMessage("persisted")
}
"signal RecoveryFailure when event handler throws during replay" in {
val probe = TestProbe[String]()
val excProbe = TestProbe[Throwable]()
val pid = PersistenceId.ofUniqueId("wrong-event-1")
val ref = spawn(failingPersistentActor(pid, probe.ref))
ref ! "malicious"
probe.expectMessage("starting")
probe.expectMessage("persisting")
probe.expectMessage("malicious")
probe.expectMessage("persisted")
LoggingTestKit.error[JournalFailureException].expect {
// start again and then the event handler will throw
spawn(failingPersistentActor(pid, probe.ref,
{
case (_, RecoveryFailed(t)) =>
excProbe.ref ! t
}))
excProbe.expectMessageType[TestException].message shouldEqual "wrong event"
probe.expectMessage("stopped")
}
}
"fail recovery if exception from RecoveryCompleted signal handler" in {
val probe = TestProbe[String]()
LoggingTestKit.error[JournalFailureException].expect {
spawn(
Behaviors
.supervise(failingPersistentActor(
PersistenceId.ofUniqueId("recovery-ok"),
probe.ref,
{
case (_, RecoveryCompleted) =>
probe.ref.tell("starting")
throw TestException("recovery call back failure")
}))
// since recovery fails restart supervision is not supposed to be used
.onFailure(SupervisorStrategy.restart))
probe.expectMessage("starting")
probe.expectMessage("stopped")
}
}
"restart with backoff" in {
val probe = TestProbe[String]()
val behav = failingPersistentActor(PersistenceId.ofUniqueId("fail-first-2"), probe.ref).onPersistFailure(
SupervisorStrategy.restartWithBackoff(1.milli, 10.millis, 0.1).withLoggingEnabled(enabled = false))
val c = spawn(behav)
probe.expectMessage("starting")
// fail
c ! "one"
probe.expectMessage("persisting")
probe.expectMessage("one")
probe.expectMessage("restarting")
probe.expectMessage("starting")
// fail
c ! "two"
probe.expectMessage("persisting")
probe.expectMessage("two")
probe.expectMessage("restarting")
probe.expectMessage("starting")
// work!
c ! "three"
probe.expectMessage("persisting")
probe.expectMessage("three")
probe.expectMessage("persisted")
// no restart
probe.expectNoMessage()
}
"restart with backoff for recovery" in {
val probe = TestProbe[String]()
val behav = failingPersistentActor(PersistenceId.ofUniqueId("fail-recovery-once"), probe.ref).onPersistFailure(
SupervisorStrategy.restartWithBackoff(1.milli, 10.millis, 0.1).withLoggingEnabled(enabled = false))
spawn(behav)
// First time fails, second time should work and call onRecoveryComplete
probe.expectMessage("restarting")
probe.expectMessage("starting")
probe.expectNoMessage()
}
"handles rejections" in {
val probe = TestProbe[String]()
val behav =
Behaviors
.supervise(failingPersistentActor(PersistenceId.ofUniqueId("reject-first"), probe.ref))
.onFailure[EventRejectedException](
SupervisorStrategy.restartWithBackoff(1.milli, 5.millis, 0.1).withLoggingEnabled(enabled = false))
val c = spawn(behav)
// First time fails, second time should work and call onRecoveryComplete
probe.expectMessage("starting")
c ! "one"
probe.expectMessage("persisting")
probe.expectMessage("one")
probe.expectMessage("restarting")
probe.expectMessage("starting")
c ! "two"
probe.expectMessage("persisting")
probe.expectMessage("two")
probe.expectMessage("persisted")
// no restart
probe.expectNoMessage()
}
"stop (default supervisor strategy) if command handler throws" in {
LoggingTestKit.error[TestException].expect {
val probe = TestProbe[String]()
val behav = failingPersistentActor(PersistenceId.ofUniqueId("wrong-command-1"), probe.ref)
val c = spawn(behav)
probe.expectMessage("starting")
c ! "wrong"
probe.expectMessage("stopped")
}
}
"restart supervisor strategy if command handler throws" in {
LoggingTestKit.error[TestException].expect {
val probe = TestProbe[String]()
val behav = Behaviors
.supervise(failingPersistentActor(PersistenceId.ofUniqueId("wrong-command-2"), probe.ref))
.onFailure[TestException](SupervisorStrategy.restart)
val c = spawn(behav)
probe.expectMessage("starting")
c ! "wrong"
probe.expectMessage("restarting")
}
}
"stop (default supervisor strategy) if side effect callback throws" in {
LoggingTestKit.error[TestException].expect {
val probe = TestProbe[String]()
val behav = failingPersistentActor(PersistenceId.ofUniqueId("wrong-command-3"), probe.ref)
val c = spawn(behav)
probe.expectMessage("starting")
c ! "wrong-callback"
probe.expectMessage("persisting")
probe.expectMessage("wrong-callback") // from event handler
probe.expectMessage("persisted")
probe.expectMessage("stopped")
}
}
"stop (default supervisor strategy) if signal handler throws" in {
case object SomeSignal extends Signal
LoggingTestKit.error[TestException].expect {
val probe = TestProbe[String]()
val behav = failingPersistentActor(PersistenceId.ofUniqueId("wrong-signal-handler"), probe.ref,
{
case (_, SomeSignal) => throw TestException("from signal")
})
val c = spawn(behav)
probe.expectMessage("starting")
c.toClassic ! SomeSignal
probe.expectMessage("stopped")
}
}
"not accept wrong event, before persisting it" in {
LoggingTestKit.error[TestException].expect {
val probe = TestProbe[String]()
val behav = failingPersistentActor(PersistenceId.ofUniqueId("wrong-event-2"), probe.ref)
val c = spawn(behav)
probe.expectMessage("starting")
// event handler will throw for this event
c ! "wrong-event"
probe.expectMessage("persisting")
// but not "persisted"
probe.expectMessage("stopped")
}
}
}
}

View file

@ -0,0 +1,113 @@
/*
* Copyright (C) 2019-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.BehaviorInterceptor
import pekko.actor.typed.TypedActorContext
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.typed.PersistenceId
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.concurrent.atomic.AtomicInteger
object EventSourcedBehaviorInterceptorSpec {
val journalId = "event-sourced-behavior-interceptor-spec"
def config: Config = ConfigFactory.parseString(s"""
pekko.loglevel = INFO
pekko.persistence.journal.plugin = "pekko.persistence.journal.inmem"
pekko.persistence.journal.inmem.test-serialization = on
""")
def testBehavior(persistenceId: PersistenceId, probe: ActorRef[String]): Behavior[String] =
Behaviors.setup { _ =>
EventSourcedBehavior[String, String, String](
persistenceId,
emptyState = "",
commandHandler = (_, command) =>
command match {
case _ =>
Effect.persist(command).thenRun(newState => probe ! newState)
},
eventHandler = (state, evt) => state + evt)
}
}
class EventSourcedBehaviorInterceptorSpec
extends ScalaTestWithActorTestKit(PersistenceTestKitPlugin.config)
with AnyWordSpecLike
with LogCapturing {
import EventSourcedBehaviorInterceptorSpec._
val pidCounter = new AtomicInteger(0)
private def nextPid(): PersistenceId = PersistenceId.ofUniqueId(s"c${pidCounter.incrementAndGet()})")
"EventSourcedBehavior interceptor" must {
"be possible to combine with another interceptor" in {
val probe = createTestProbe[String]()
val pid = nextPid()
val toUpper = new BehaviorInterceptor[String, String] {
override def aroundReceive(
ctx: TypedActorContext[String],
msg: String,
target: BehaviorInterceptor.ReceiveTarget[String]): Behavior[String] = {
target(ctx, msg.toUpperCase())
}
}
val ref = spawn(Behaviors.intercept(() => toUpper)(testBehavior(pid, probe.ref)))
ref ! "a"
ref ! "bc"
probe.expectMessage("A")
probe.expectMessage("ABC")
}
"be possible to combine with transformMessages" in {
val probe = createTestProbe[String]()
val pid = nextPid()
val ref = spawn(testBehavior(pid, probe.ref).transformMessages[String] {
case s => s.toUpperCase()
})
ref ! "a"
ref ! "bc"
probe.expectMessage("A")
probe.expectMessage("ABC")
}
"be possible to combine with MDC" in {
val probe = createTestProbe[String]()
val pid = nextPid()
val ref = spawn(Behaviors.setup[String] { _ =>
Behaviors.withMdc(
staticMdc = Map("pid" -> pid.toString),
mdcForMessage = (msg: String) => Map("msg" -> msg.toUpperCase())) {
testBehavior(pid, probe.ref)
}
})
ref ! "a"
ref ! "bc"
probe.expectMessage("a")
probe.expectMessage("abc")
}
}
}

View file

@ -0,0 +1,107 @@
/*
* Copyright (C) 2019-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.RecoveryTimedOut
import pekko.persistence.journal.SteppingInmemJournal
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.RecoveryFailed
import pekko.persistence.typed.internal.JournalFailureException
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.concurrent.atomic.AtomicInteger
import scala.concurrent.duration._
object EventSourcedBehaviorRecoveryTimeoutSpec {
val journalId = "event-sourced-behavior-recovery-timeout-spec"
def config: Config =
SteppingInmemJournal
.config(journalId)
.withFallback(ConfigFactory.parseString("""
pekko.persistence.journal.stepping-inmem.recovery-event-timeout=1s
"""))
.withFallback(ConfigFactory.parseString(s"""
pekko.loglevel = INFO
"""))
def testBehavior(persistenceId: PersistenceId, probe: ActorRef[AnyRef]): Behavior[String] =
Behaviors.setup { _ =>
EventSourcedBehavior[String, String, String](
persistenceId,
emptyState = "",
commandHandler = (_, command) => Effect.persist(command).thenRun(_ => probe ! command),
eventHandler = (state, evt) => state + evt).receiveSignal {
case (_, RecoveryFailed(cause)) =>
probe ! cause
}
}
}
class EventSourcedBehaviorRecoveryTimeoutSpec
extends ScalaTestWithActorTestKit(EventSourcedBehaviorRecoveryTimeoutSpec.config)
with AnyWordSpecLike
with LogCapturing {
import EventSourcedBehaviorRecoveryTimeoutSpec._
val pidCounter = new AtomicInteger(0)
private def nextPid(): PersistenceId = PersistenceId.ofUniqueId(s"c${pidCounter.incrementAndGet()})")
import org.apache.pekko.actor.typed.scaladsl.adapter._
// needed for SteppingInmemJournal.step
private implicit val classicSystem: pekko.actor.ActorSystem = system.toClassic
"The recovery timeout" must {
"fail recovery if timeout is not met when recovering" in {
val probe = createTestProbe[AnyRef]()
val pid = nextPid()
val persisting = spawn(testBehavior(pid, probe.ref))
probe.awaitAssert(SteppingInmemJournal.getRef(journalId), 3.seconds)
val journal = SteppingInmemJournal.getRef(journalId)
// initial read highest
SteppingInmemJournal.step(journal)
persisting ! "A"
SteppingInmemJournal.step(journal)
probe.expectMessage("A")
testKit.stop(persisting)
probe.expectTerminated(persisting)
// now replay, but don't give the journal any tokens to replay events
// so that we cause the timeout to trigger
LoggingTestKit
.error[JournalFailureException]
.withMessageRegex("Exception during recovery.*Replay timed out")
.expect {
val replaying = spawn(testBehavior(pid, probe.ref))
// initial read highest
SteppingInmemJournal.step(journal)
probe.expectMessageType[RecoveryTimedOut]
probe.expectTerminated(replaying)
}
// avoid having it stuck in the next test from the
// last read request above
SteppingInmemJournal.step(journal)
}
}
}

View file

@ -0,0 +1,108 @@
/*
* Copyright (C) 2017-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.Done
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.ActorContext
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.typed.PersistenceId
import pekko.serialization.jackson.CborSerializable
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.concurrent.atomic.AtomicInteger
object EventSourcedBehaviorReplySpec {
sealed trait Command[ReplyMessage] extends CborSerializable
final case class IncrementWithConfirmation(replyTo: ActorRef[Done]) extends Command[Done]
final case class IncrementReplyLater(replyTo: ActorRef[Done]) extends Command[Done]
final case class ReplyNow(replyTo: ActorRef[Done]) extends Command[Done]
final case class GetValue(replyTo: ActorRef[State]) extends Command[State]
sealed trait Event extends CborSerializable
final case class Incremented(delta: Int) extends Event
final case class State(value: Int, history: Vector[Int]) extends CborSerializable
def counter(persistenceId: PersistenceId): Behavior[Command[_]] =
Behaviors.setup(ctx => counter(ctx, persistenceId))
def counter(
ctx: ActorContext[Command[_]],
persistenceId: PersistenceId): EventSourcedBehavior[Command[_], Event, State] = {
EventSourcedBehavior.withEnforcedReplies[Command[_], Event, State](
persistenceId,
emptyState = State(0, Vector.empty),
commandHandler = (state, command) =>
command match {
case IncrementWithConfirmation(replyTo) =>
Effect.persist(Incremented(1)).thenReply(replyTo)(_ => Done)
case IncrementReplyLater(replyTo) =>
Effect.persist(Incremented(1)).thenRun((_: State) => ctx.self ! ReplyNow(replyTo)).thenNoReply()
case ReplyNow(replyTo) =>
Effect.reply(replyTo)(Done)
case GetValue(replyTo) =>
Effect.reply(replyTo)(state)
},
eventHandler = (state, evt) =>
evt match {
case Incremented(delta) =>
State(state.value + delta, state.history :+ state.value)
})
}
}
class EventSourcedBehaviorReplySpec
extends ScalaTestWithActorTestKit(PersistenceTestKitPlugin.config)
with AnyWordSpecLike
with LogCapturing {
import EventSourcedBehaviorReplySpec._
val pidCounter = new AtomicInteger(0)
private def nextPid(): PersistenceId = PersistenceId.ofUniqueId(s"c${pidCounter.incrementAndGet()})")
"A typed persistent actor with commands that are expecting replies" must {
"persist an event thenReply" in {
val c = spawn(counter(nextPid()))
val probe = TestProbe[Done]()
c ! IncrementWithConfirmation(probe.ref)
probe.expectMessage(Done)
c ! IncrementWithConfirmation(probe.ref)
c ! IncrementWithConfirmation(probe.ref)
probe.expectMessage(Done)
probe.expectMessage(Done)
}
"persist an event thenReply later" in {
val c = spawn(counter(nextPid()))
val probe = TestProbe[Done]()
c ! IncrementReplyLater(probe.ref)
probe.expectMessage(Done)
}
"reply to query command" in {
val c = spawn(counter(nextPid()))
val updateProbe = TestProbe[Done]()
c ! IncrementWithConfirmation(updateProbe.ref)
val queryProbe = TestProbe[State]()
c ! GetValue(queryProbe.ref)
queryProbe.expectMessage(State(1, Vector(0)))
}
}
}

View file

@ -0,0 +1,594 @@
/*
* Copyright (C) 2017-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.ActorContext
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.testkit.PersistenceTestKitSnapshotPlugin
import pekko.persistence.typed.DeleteEventsCompleted
import pekko.persistence.typed.DeleteSnapshotsCompleted
import pekko.persistence.typed.DeleteSnapshotsFailed
import pekko.persistence.typed.DeletionTarget
import pekko.persistence.typed.EventSourcedSignal
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.RecoveryCompleted
import pekko.persistence.typed.SnapshotCompleted
import pekko.persistence.typed.SnapshotFailed
import pekko.persistence.typed.SnapshotSelectionCriteria
import pekko.serialization.jackson.CborSerializable
import pekko.util.unused
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.concurrent.atomic.AtomicInteger
import scala.concurrent.duration._
import scala.util.Success
import scala.util.Try
object EventSourcedBehaviorRetentionSpec extends Matchers {
sealed trait Command extends CborSerializable
case object Increment extends Command
final case class IncrementWithPersistAll(nr: Int) extends Command
final case class GetValue(replyTo: ActorRef[State]) extends Command
case object StopIt extends Command
final case class WrappedSignal(signal: EventSourcedSignal)
sealed trait Event extends CborSerializable
final case class Incremented(delta: Int) extends Event
final case class State(value: Int, history: Vector[Int]) extends CborSerializable
def counter(
@unused ctx: ActorContext[Command],
persistenceId: PersistenceId,
probe: Option[ActorRef[(State, Event)]] = None,
snapshotSignalProbe: Option[ActorRef[WrappedSignal]] = None,
eventSignalProbe: Option[ActorRef[Try[EventSourcedSignal]]] = None)
: EventSourcedBehavior[Command, Event, State] = {
EventSourcedBehavior[Command, Event, State](
persistenceId,
emptyState = State(0, Vector.empty),
commandHandler = (state, cmd) =>
cmd match {
case Increment =>
Effect.persist(Incremented(1))
case IncrementWithPersistAll(n) =>
Effect.persist((0 until n).map(_ => Incremented(1)))
case GetValue(replyTo) =>
replyTo ! state
Effect.none
case StopIt =>
Effect.none.thenStop()
},
eventHandler = (state, evt) =>
evt match {
case Incremented(delta) =>
probe.foreach(_ ! ((state, evt)))
State(state.value + delta, state.history :+ state.value)
}).receiveSignal {
case (_, RecoveryCompleted) => ()
case (_, sc: SnapshotCompleted) =>
snapshotSignalProbe.foreach(_ ! WrappedSignal(sc))
case (_, sf: SnapshotFailed) =>
snapshotSignalProbe.foreach(_ ! WrappedSignal(sf))
case (_, dc: DeleteSnapshotsCompleted) =>
snapshotSignalProbe.foreach(_ ! WrappedSignal(dc))
case (_, dsf: DeleteSnapshotsFailed) =>
snapshotSignalProbe.foreach(_ ! WrappedSignal(dsf))
case (_, e: EventSourcedSignal) =>
eventSignalProbe.foreach(_ ! Success(e))
}
}
implicit class WrappedSignalProbeAssert(probe: TestProbe[WrappedSignal]) {
def expectSnapshotCompleted(sequenceNumber: Int): SnapshotCompleted = {
val wrapped = probe.expectMessageType[WrappedSignal]
wrapped.signal shouldBe a[SnapshotCompleted]
val completed = wrapped.signal.asInstanceOf[SnapshotCompleted]
completed.metadata.sequenceNr should ===(sequenceNumber)
completed
}
def expectDeleteSnapshotCompleted(maxSequenceNr: Long, minSequenceNr: Long): DeleteSnapshotsCompleted = {
val wrapped = probe.expectMessageType[WrappedSignal]
wrapped.signal shouldBe a[DeleteSnapshotsCompleted]
val signal = wrapped.signal.asInstanceOf[DeleteSnapshotsCompleted]
signal.target should ===(
DeletionTarget.Criteria(
SnapshotSelectionCriteria.latest.withMaxSequenceNr(maxSequenceNr).withMinSequenceNr(minSequenceNr)))
signal
}
}
}
class EventSourcedBehaviorRetentionSpec
extends ScalaTestWithActorTestKit(
PersistenceTestKitPlugin.config.withFallback(PersistenceTestKitSnapshotPlugin.config))
with AnyWordSpecLike
with LogCapturing {
import EventSourcedBehaviorRetentionSpec._
val pidCounter = new AtomicInteger(0)
private def nextPid(): PersistenceId = PersistenceId.ofUniqueId(s"c${pidCounter.incrementAndGet()})")
"EventSourcedBehavior with retention" must {
"snapshot every N sequence nrs" in {
val pid = nextPid()
val c = spawn(Behaviors.setup[Command](ctx =>
counter(ctx, pid).withRetention(RetentionCriteria.snapshotEvery(numberOfEvents = 2, keepNSnapshots = 2))))
val replyProbe = TestProbe[State]()
c ! Increment
c ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(1, Vector(0)))
c ! StopIt
val watchProbe = TestProbe()
watchProbe.expectTerminated(c)
// no snapshot should have happened
val probeC2 = TestProbe[(State, Event)]()
val snapshotProbe = createTestProbe[WrappedSignal]()
val c2 = spawn(
Behaviors.setup[Command](ctx =>
counter(ctx, pid, probe = Some(probeC2.ref), snapshotSignalProbe = Some(snapshotProbe.ref))
.withRetention(RetentionCriteria.snapshotEvery(numberOfEvents = 2, keepNSnapshots = 2))))
probeC2.expectMessage[(State, Event)]((State(0, Vector()), Incremented(1)))
c2 ! Increment
snapshotProbe.expectSnapshotCompleted(2)
c2 ! StopIt
watchProbe.expectTerminated(c2)
val probeC3 = TestProbe[(State, Event)]()
val c3 = spawn(
Behaviors.setup[Command](ctx =>
counter(ctx, pid, Some(probeC3.ref))
.withRetention(RetentionCriteria.snapshotEvery(numberOfEvents = 2, keepNSnapshots = 2))))
// this time it should have been snapshotted so no events to replay
probeC3.expectNoMessage()
c3 ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(2, Vector(0, 1)))
}
"snapshot every N sequence nrs when persisting multiple events" in {
val pid = nextPid()
val snapshotSignalProbe = TestProbe[WrappedSignal]()
val c =
spawn(
Behaviors.setup[Command](ctx =>
counter(ctx, pid, None, Some(snapshotSignalProbe.ref))
.withRetention(RetentionCriteria.snapshotEvery(numberOfEvents = 2, keepNSnapshots = 2))))
val replyProbe = TestProbe[State]()
c ! IncrementWithPersistAll(3)
c ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(3, Vector(0, 1, 2)))
// snapshot at seqNr 3 because of persistAll
snapshotSignalProbe.expectSnapshotCompleted(3)
c ! StopIt
val watchProbe = TestProbe()
watchProbe.expectTerminated(c)
val probeC2 = TestProbe[(State, Event)]()
val c2 = spawn(
Behaviors.setup[Command](ctx =>
counter(ctx, pid, Some(probeC2.ref))
.withRetention(RetentionCriteria.snapshotEvery(numberOfEvents = 2, keepNSnapshots = 2))))
probeC2.expectNoMessage()
c2 ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(3, Vector(0, 1, 2)))
}
"snapshot via predicate" in {
val pid = nextPid()
val snapshotSignalProbe = TestProbe[WrappedSignal]()
val alwaysSnapshot: Behavior[Command] =
Behaviors.setup { ctx =>
counter(ctx, pid, snapshotSignalProbe = Some(snapshotSignalProbe.ref)).snapshotWhen { (_, _, _) =>
true
}
}
val c = spawn(alwaysSnapshot)
val replyProbe = TestProbe[State]()
c ! Increment
snapshotSignalProbe.expectSnapshotCompleted(1)
c ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(1, Vector(0)))
c ! StopIt
val watchProbe = TestProbe()
watchProbe.expectTerminated(c)
val probe = TestProbe[(State, Event)]()
val c2 = spawn(Behaviors.setup[Command](ctx => counter(ctx, pid, Some(probe.ref))))
// state should be rebuilt from snapshot, no events replayed
// Fails as snapshot is async (i think)
probe.expectNoMessage()
c2 ! Increment
c2 ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(2, Vector(0, 1)))
}
"check all events for snapshot in PersistAll" in {
val pid = nextPid()
val snapshotSignalProbe = TestProbe[WrappedSignal]()
val snapshotAtTwo = Behaviors.setup[Command](ctx =>
counter(ctx, pid, snapshotSignalProbe = Some(snapshotSignalProbe.ref)).snapshotWhen { (s, _, _) =>
s.value == 2
})
val c: ActorRef[Command] = spawn(snapshotAtTwo)
val replyProbe = TestProbe[State]()
c ! IncrementWithPersistAll(3)
c ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(3, Vector(0, 1, 2)))
// snapshot at seqNr 3 because of persistAll
snapshotSignalProbe.expectSnapshotCompleted(3)
c ! StopIt
val watchProbe = TestProbe()
watchProbe.expectTerminated(c)
val probeC2 = TestProbe[(State, Event)]()
val c2 = spawn(Behaviors.setup[Command](ctx => counter(ctx, pid, Some(probeC2.ref))))
// middle event triggered all to be snapshot
probeC2.expectNoMessage()
c2 ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(3, Vector(0, 1, 2)))
}
"delete snapshots automatically, based on criteria" in {
val pid = nextPid()
val snapshotSignalProbe = TestProbe[WrappedSignal]()
val replyProbe = TestProbe[State]()
val persistentActor = spawn(
Behaviors.setup[Command](ctx =>
counter(ctx, pid, snapshotSignalProbe = Some(snapshotSignalProbe.ref))
.withRetention(RetentionCriteria.snapshotEvery(numberOfEvents = 3, keepNSnapshots = 2))))
(1 to 10).foreach(_ => persistentActor ! Increment)
persistentActor ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(10, (0 until 10).toVector))
snapshotSignalProbe.expectSnapshotCompleted(3)
snapshotSignalProbe.expectSnapshotCompleted(6)
snapshotSignalProbe.expectSnapshotCompleted(9)
snapshotSignalProbe.expectDeleteSnapshotCompleted(3, 0)
(1 to 10).foreach(_ => persistentActor ! Increment)
persistentActor ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(20, (0 until 20).toVector))
snapshotSignalProbe.expectSnapshotCompleted(12)
snapshotSignalProbe.expectDeleteSnapshotCompleted(6, 0)
snapshotSignalProbe.expectSnapshotCompleted(15)
snapshotSignalProbe.expectDeleteSnapshotCompleted(9, 3)
snapshotSignalProbe.expectSnapshotCompleted(18)
snapshotSignalProbe.expectDeleteSnapshotCompleted(12, 6)
snapshotSignalProbe.expectNoMessage()
}
"optionally delete both old events and snapshots" in {
val pid = nextPid()
val snapshotSignalProbe = TestProbe[WrappedSignal]()
val eventProbe = TestProbe[Try[EventSourcedSignal]]()
val replyProbe = TestProbe[State]()
val persistentActor = spawn(Behaviors.setup[Command](ctx =>
counter(ctx, pid, snapshotSignalProbe = Some(snapshotSignalProbe.ref), eventSignalProbe = Some(eventProbe.ref))
.withRetention(
// tests the Java API as well
RetentionCriteria.snapshotEvery(numberOfEvents = 3, keepNSnapshots = 2).withDeleteEventsOnSnapshot)))
(1 to 10).foreach(_ => persistentActor ! Increment)
persistentActor ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(10, (0 until 10).toVector))
snapshotSignalProbe.expectSnapshotCompleted(3)
snapshotSignalProbe.expectSnapshotCompleted(6)
snapshotSignalProbe.expectSnapshotCompleted(9)
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 3
// Note that when triggering deletion of snapshots from deletion of events it is intentionally "off by one".
// The reason for -1 is that a snapshot at the exact toSequenceNr is still useful and the events
// after that can be replayed after that snapshot, but replaying the events after toSequenceNr without
// starting at the snapshot at toSequenceNr would be invalid.
snapshotSignalProbe.expectDeleteSnapshotCompleted(2, 0)
// one at a time since snapshotting+event-deletion switches to running state before deleting snapshot so ordering
// if sending many commands in one go is not deterministic
persistentActor ! Increment // 11
persistentActor ! Increment // 12
snapshotSignalProbe.expectSnapshotCompleted(12)
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 6
snapshotSignalProbe.expectDeleteSnapshotCompleted(5, 0)
persistentActor ! Increment // 13
persistentActor ! Increment // 14
persistentActor ! Increment // 11
persistentActor ! Increment // 15
snapshotSignalProbe.expectSnapshotCompleted(15)
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 9
snapshotSignalProbe.expectDeleteSnapshotCompleted(8, 2)
persistentActor ! Increment // 16
persistentActor ! Increment // 17
persistentActor ! Increment // 18
snapshotSignalProbe.expectSnapshotCompleted(18)
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 12
snapshotSignalProbe.expectDeleteSnapshotCompleted(11, 5)
eventProbe.expectNoMessage()
snapshotSignalProbe.expectNoMessage()
}
"be possible to combine snapshotWhen and retention criteria" in {
val pid = nextPid()
val snapshotSignalProbe = TestProbe[WrappedSignal]()
val eventProbe = TestProbe[Try[EventSourcedSignal]]()
val replyProbe = TestProbe[State]()
val persistentActor = spawn(Behaviors.setup[Command](ctx =>
counter(ctx, pid, snapshotSignalProbe = Some(snapshotSignalProbe.ref), eventSignalProbe = Some(eventProbe.ref))
.snapshotWhen((_, _, seqNr) => seqNr == 3 || seqNr == 13)
.withRetention(RetentionCriteria.snapshotEvery(numberOfEvents = 5, keepNSnapshots = 1))))
(1 to 3).foreach(_ => persistentActor ! Increment)
persistentActor ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(3, (0 until 3).toVector))
snapshotSignalProbe.expectSnapshotCompleted(3)
eventProbe.expectNoMessage()
(4 to 10).foreach(_ => persistentActor ! Increment)
snapshotSignalProbe.expectSnapshotCompleted(5)
snapshotSignalProbe.expectSnapshotCompleted(10)
snapshotSignalProbe.expectDeleteSnapshotCompleted(5, 0)
(11 to 13).foreach(_ => persistentActor ! Increment)
snapshotSignalProbe.expectSnapshotCompleted(13)
// no deletes triggered by snapshotWhen
eventProbe.within(3.seconds) {
eventProbe.expectNoMessage()
snapshotSignalProbe.expectNoMessage()
}
(14 to 16).foreach(_ => persistentActor ! Increment)
persistentActor ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(16, (0 until 16).toVector))
snapshotSignalProbe.expectSnapshotCompleted(15)
snapshotSignalProbe.expectDeleteSnapshotCompleted(10, 5)
eventProbe.within(3.seconds) {
eventProbe.expectNoMessage()
snapshotSignalProbe.expectNoMessage()
}
}
"be possible to combine snapshotWhen and retention criteria withDeleteEventsOnSnapshot" in {
val pid = nextPid()
val snapshotSignalProbe = TestProbe[WrappedSignal]()
val eventProbe = TestProbe[Try[EventSourcedSignal]]()
val replyProbe = TestProbe[State]()
val persistentActor = spawn(Behaviors.setup[Command](ctx =>
counter(ctx, pid, snapshotSignalProbe = Some(snapshotSignalProbe.ref), eventSignalProbe = Some(eventProbe.ref))
.snapshotWhen((_, _, seqNr) => seqNr == 3 || seqNr == 13)
.withRetention(
RetentionCriteria.snapshotEvery(numberOfEvents = 2, keepNSnapshots = 3).withDeleteEventsOnSnapshot)))
(1 to 3).foreach(_ => persistentActor ! Increment)
persistentActor ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(3, (0 until 3).toVector))
snapshotSignalProbe.expectSnapshotCompleted(2) // every-2 through criteria
snapshotSignalProbe.expectSnapshotCompleted(3) // snapshotWhen
// no event deletes or snapshot deletes after snapshotWhen
eventProbe.within(3.seconds) {
eventProbe.expectNoMessage()
snapshotSignalProbe.expectNoMessage()
}
// one at a time since snapshotting+event-deletion switches to running state before deleting snapshot so ordering
// if sending many commands in one go is not deterministic
persistentActor ! Increment // 4
snapshotSignalProbe.expectSnapshotCompleted(4) // every-2 through criteria
persistentActor ! Increment // 5
persistentActor ! Increment // 6
snapshotSignalProbe.expectSnapshotCompleted(6) // every-2 through criteria
persistentActor ! Increment // 7
persistentActor ! Increment // 8
snapshotSignalProbe.expectSnapshotCompleted(8) // every-2 through criteria
// triggers delete up to snapshot no 2
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 2
snapshotSignalProbe.expectDeleteSnapshotCompleted(1, 0) // then delete oldest snapshot
persistentActor ! Increment // 9
persistentActor ! Increment // 10
snapshotSignalProbe.expectSnapshotCompleted(10) // every-2 through criteria
snapshotSignalProbe.expectDeleteSnapshotCompleted(3, 0)
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 4
persistentActor ! Increment // 11
persistentActor ! Increment // 12
snapshotSignalProbe.expectSnapshotCompleted(12) // every-2 through criteria
snapshotSignalProbe.expectDeleteSnapshotCompleted(5, 0)
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 6
persistentActor ! Increment // 13
snapshotSignalProbe.expectSnapshotCompleted(13) // snapshotWhen
// no deletes triggered by snapshotWhen
eventProbe.within(3.seconds) {
eventProbe.expectNoMessage()
snapshotSignalProbe.expectNoMessage()
}
persistentActor ! Increment // 14
snapshotSignalProbe.expectSnapshotCompleted(14) // every-2 through criteria
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 8
snapshotSignalProbe.expectDeleteSnapshotCompleted(7, 1)
persistentActor ! Increment // 15
persistentActor ! Increment // 16
snapshotSignalProbe.expectSnapshotCompleted(16) // every-2 through criteria
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 10
snapshotSignalProbe.expectDeleteSnapshotCompleted(9, 3)
eventProbe.within(3.seconds) {
eventProbe.expectNoMessage()
snapshotSignalProbe.expectNoMessage()
}
}
"be possible to snapshot every event" in {
// very bad idea to snapshot every event, but technically possible
val pid = nextPid()
val snapshotSignalProbe = TestProbe[WrappedSignal]()
val replyProbe = TestProbe[State]()
val persistentActor = spawn(
Behaviors.setup[Command](ctx =>
counter(ctx, pid, snapshotSignalProbe = Some(snapshotSignalProbe.ref))
.withRetention(RetentionCriteria.snapshotEvery(numberOfEvents = 1, keepNSnapshots = 3))))
(1 to 10).foreach(_ => persistentActor ! Increment)
persistentActor ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(10, (0 until 10).toVector))
snapshotSignalProbe.expectSnapshotCompleted(1)
snapshotSignalProbe.expectSnapshotCompleted(2)
snapshotSignalProbe.expectSnapshotCompleted(3)
snapshotSignalProbe.expectSnapshotCompleted(4)
snapshotSignalProbe.expectDeleteSnapshotCompleted(1, 0)
snapshotSignalProbe.expectSnapshotCompleted(5)
snapshotSignalProbe.expectDeleteSnapshotCompleted(2, 0)
snapshotSignalProbe.expectSnapshotCompleted(6)
snapshotSignalProbe.expectDeleteSnapshotCompleted(3, 0)
snapshotSignalProbe.expectSnapshotCompleted(7)
snapshotSignalProbe.expectDeleteSnapshotCompleted(4, 1)
snapshotSignalProbe.expectSnapshotCompleted(8)
snapshotSignalProbe.expectDeleteSnapshotCompleted(5, 2)
snapshotSignalProbe.expectSnapshotCompleted(9)
snapshotSignalProbe.expectDeleteSnapshotCompleted(6, 3)
snapshotSignalProbe.expectSnapshotCompleted(10)
snapshotSignalProbe.expectDeleteSnapshotCompleted(7, 4)
}
"be possible to snapshot every event withDeleteEventsOnSnapshot" in {
// very bad idea to snapshot every event, but technically possible
val pid = nextPid()
val snapshotSignalProbe = TestProbe[WrappedSignal]()
val eventProbe = TestProbe[Try[EventSourcedSignal]]()
val persistentActor = spawn(Behaviors.setup[Command](ctx =>
counter(ctx, pid, snapshotSignalProbe = Some(snapshotSignalProbe.ref), eventSignalProbe = Some(eventProbe.ref))
.withRetention(
RetentionCriteria.snapshotEvery(numberOfEvents = 1, keepNSnapshots = 3).withDeleteEventsOnSnapshot)))
// one at a time since snapshotting+event-deletion switches to running state before deleting snapshot so ordering
// if sending many commands in one go is not deterministic
(1 to 4).foreach(_ => persistentActor ! Increment)
snapshotSignalProbe.expectSnapshotCompleted(1)
snapshotSignalProbe.expectSnapshotCompleted(2)
snapshotSignalProbe.expectSnapshotCompleted(3)
snapshotSignalProbe.expectSnapshotCompleted(4)
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 1
persistentActor ! Increment // 5
snapshotSignalProbe.expectSnapshotCompleted(5)
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 2
snapshotSignalProbe.expectDeleteSnapshotCompleted(1, 0)
persistentActor ! Increment // 6
snapshotSignalProbe.expectSnapshotCompleted(6)
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 3
snapshotSignalProbe.expectDeleteSnapshotCompleted(2, 0)
persistentActor ! Increment // 7
snapshotSignalProbe.expectSnapshotCompleted(7)
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 4
snapshotSignalProbe.expectDeleteSnapshotCompleted(3, 0)
persistentActor ! Increment // 8
snapshotSignalProbe.expectSnapshotCompleted(8)
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 5
snapshotSignalProbe.expectDeleteSnapshotCompleted(4, 1)
persistentActor ! Increment // 9
snapshotSignalProbe.expectSnapshotCompleted(9)
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 6
snapshotSignalProbe.expectDeleteSnapshotCompleted(5, 2)
persistentActor ! Increment // 10
snapshotSignalProbe.expectSnapshotCompleted(10)
eventProbe.expectMessageType[Success[DeleteEventsCompleted]].value.toSequenceNr shouldEqual 7
snapshotSignalProbe.expectDeleteSnapshotCompleted(6, 3)
}
"snapshot on recovery if expected snapshot is missing" in {
val pid = nextPid()
val snapshotSignalProbe = TestProbe[WrappedSignal]()
{
val persistentActor =
spawn(Behaviors.setup[Command](ctx => counter(ctx, pid, snapshotSignalProbe = Some(snapshotSignalProbe.ref))))
(1 to 5).foreach(_ => persistentActor ! Increment)
snapshotSignalProbe.expectNoMessage()
persistentActor ! StopIt
val watchProbe = TestProbe()
watchProbe.expectTerminated(persistentActor)
}
{
val persistentActor = spawn(
Behaviors.setup[Command](ctx =>
counter(ctx, pid, snapshotSignalProbe = Some(snapshotSignalProbe.ref))
.withRetention(RetentionCriteria.snapshotEvery(numberOfEvents = 5, keepNSnapshots = 1))))
val replyProbe = TestProbe[State]()
persistentActor ! GetValue(replyProbe.ref)
snapshotSignalProbe.expectSnapshotCompleted(5)
replyProbe.expectMessage(State(5, Vector(0, 1, 2, 3, 4)))
persistentActor ! StopIt
val watchProbe = TestProbe()
watchProbe.expectTerminated(persistentActor)
}
{
val persistentActor = spawn(
Behaviors.setup[Command](ctx =>
counter(ctx, pid, snapshotSignalProbe = Some(snapshotSignalProbe.ref))
.withRetention(RetentionCriteria.snapshotEvery(numberOfEvents = 5, keepNSnapshots = 1))))
val replyProbe = TestProbe[State]()
persistentActor ! GetValue(replyProbe.ref)
snapshotSignalProbe.expectNoMessage()
replyProbe.expectMessage(State(5, Vector(0, 1, 2, 3, 4)))
}
}
}
}

View file

@ -0,0 +1,740 @@
/*
* Copyright (C) 2017-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.Done
import pekko.actor.ActorInitializationException
import pekko.actor.testkit.typed.TestException
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.ActorSystem
import pekko.actor.typed.Behavior
import pekko.actor.typed.SupervisorStrategy
import pekko.actor.typed.Terminated
import pekko.actor.typed.scaladsl.ActorContext
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.SelectedSnapshot
import pekko.persistence.journal.inmem.InmemJournal
import pekko.persistence.query.EventEnvelope
import pekko.persistence.query.Offset
import pekko.persistence.query.PersistenceQuery
import pekko.persistence.query.Sequence
import pekko.persistence.snapshot.SnapshotStore
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.RecoveryCompleted
import pekko.persistence.typed.SnapshotCompleted
import pekko.persistence.typed.SnapshotFailed
import pekko.persistence.typed.SnapshotMetadata
import pekko.persistence.typed.SnapshotSelectionCriteria
import pekko.persistence.{ SnapshotMetadata => ClassicSnapshotMetadata }
import pekko.persistence.{ SnapshotSelectionCriteria => ClassicSnapshotSelectionCriteria }
import pekko.serialization.jackson.CborSerializable
import pekko.stream.scaladsl.Sink
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.UUID
import java.util.concurrent.atomic.AtomicInteger
import scala.concurrent.Future
import scala.concurrent.Promise
import scala.concurrent.duration._
import scala.util.Failure
import scala.util.Success
import scala.util.Try
object EventSourcedBehaviorSpec {
class SlowInMemorySnapshotStore extends SnapshotStore {
private var state = Map.empty[String, (Any, ClassicSnapshotMetadata)]
override def loadAsync(
persistenceId: String,
criteria: ClassicSnapshotSelectionCriteria): Future[Option[SelectedSnapshot]] = {
Promise().future // never completed
}
override def saveAsync(metadata: ClassicSnapshotMetadata, snapshot: Any): Future[Unit] = {
state = state.updated(metadata.persistenceId, (snapshot, metadata))
Future.successful(())
}
override def deleteAsync(metadata: ClassicSnapshotMetadata): Future[Unit] = {
state = state.filterNot { case (k, (_, b)) => k == metadata.persistenceId && b.sequenceNr == metadata.sequenceNr }
Future.successful(())
}
override def deleteAsync(persistenceId: String, criteria: ClassicSnapshotSelectionCriteria): Future[Unit] = {
val range = criteria.minSequenceNr to criteria.maxSequenceNr
state = state.filterNot { case (k, (_, b)) => k == persistenceId && range.contains(b.sequenceNr) }
Future.successful(())
}
}
// also used from PersistentActorTest, EventSourcedBehaviorWatchSpec
def conf: Config = PersistenceTestKitPlugin.config.withFallback(ConfigFactory.parseString(s"""
pekko.loglevel = INFO
# pekko.persistence.typed.log-stashing = on
pekko.persistence.snapshot-store.plugin = "pekko.persistence.snapshot-store.local"
pekko.persistence.snapshot-store.local.dir = "target/typed-persistence-${UUID.randomUUID().toString}"
slow-snapshot-store.class = "${classOf[SlowInMemorySnapshotStore].getName}"
short-recovery-timeout {
class = "${classOf[InmemJournal].getName}"
recovery-event-timeout = 10 millis
}
"""))
sealed trait Command extends CborSerializable
case object Increment extends Command
case object IncrementThenLogThenStop extends Command
case object IncrementTwiceThenLogThenStop extends Command
final case class IncrementWithPersistAll(nr: Int) extends Command
case object IncrementLater extends Command
case object IncrementAfterReceiveTimeout extends Command
case object IncrementTwiceAndThenLog extends Command
final case class IncrementWithConfirmation(replyTo: ActorRef[Done]) extends Command
case object DoNothingAndThenLog extends Command
case object EmptyEventsListAndThenLog extends Command
final case class GetValue(replyTo: ActorRef[State]) extends Command
case object DelayFinished extends Command
private case object Timeout extends Command
case object LogThenStop extends Command
case object Fail extends Command
case object StopIt extends Command
sealed trait Event extends CborSerializable
final case class Incremented(delta: Int) extends Event
final case class State(value: Int, history: Vector[Int]) extends CborSerializable
case object Tick
val firstLogging = "first logging"
val secondLogging = "second logging"
def counter(persistenceId: PersistenceId)(implicit system: ActorSystem[_]): Behavior[Command] =
Behaviors.setup(ctx => counter(ctx, persistenceId))
def counter(persistenceId: PersistenceId, logging: ActorRef[String])(
implicit system: ActorSystem[_]): Behavior[Command] =
Behaviors.setup(ctx => counter(ctx, persistenceId, logging))
def counter(ctx: ActorContext[Command], persistenceId: PersistenceId)(
implicit system: ActorSystem[_]): EventSourcedBehavior[Command, Event, State] =
counter(
ctx,
persistenceId,
loggingActor = TestProbe[String]().ref,
probe = TestProbe[(State, Event)]().ref,
snapshotProbe = TestProbe[Try[SnapshotMetadata]]().ref)
def counter(ctx: ActorContext[Command], persistenceId: PersistenceId, logging: ActorRef[String])(
implicit system: ActorSystem[_]): EventSourcedBehavior[Command, Event, State] =
counter(
ctx,
persistenceId,
loggingActor = logging,
probe = TestProbe[(State, Event)]().ref,
TestProbe[Try[SnapshotMetadata]]().ref)
def counterWithProbe(
ctx: ActorContext[Command],
persistenceId: PersistenceId,
probe: ActorRef[(State, Event)],
snapshotProbe: ActorRef[Try[SnapshotMetadata]])(
implicit system: ActorSystem[_]): EventSourcedBehavior[Command, Event, State] =
counter(ctx, persistenceId, TestProbe[String]().ref, probe, snapshotProbe)
def counterWithProbe(ctx: ActorContext[Command], persistenceId: PersistenceId, probe: ActorRef[(State, Event)])(
implicit system: ActorSystem[_]): EventSourcedBehavior[Command, Event, State] =
counter(ctx, persistenceId, TestProbe[String]().ref, probe, TestProbe[Try[SnapshotMetadata]]().ref)
def counterWithSnapshotProbe(
ctx: ActorContext[Command],
persistenceId: PersistenceId,
probe: ActorRef[Try[SnapshotMetadata]])(
implicit system: ActorSystem[_]): EventSourcedBehavior[Command, Event, State] =
counter(ctx, persistenceId, TestProbe[String]().ref, TestProbe[(State, Event)]().ref, snapshotProbe = probe)
def counter(
ctx: ActorContext[Command],
persistenceId: PersistenceId,
loggingActor: ActorRef[String],
probe: ActorRef[(State, Event)],
snapshotProbe: ActorRef[Try[SnapshotMetadata]]): EventSourcedBehavior[Command, Event, State] = {
EventSourcedBehavior[Command, Event, State](
persistenceId,
emptyState = State(0, Vector.empty),
commandHandler = (state, cmd) =>
cmd match {
case Increment =>
Effect.persist(Incremented(1))
case IncrementThenLogThenStop =>
Effect
.persist(Incremented(1))
.thenRun { (_: State) =>
loggingActor ! firstLogging
}
.thenStop()
case IncrementTwiceThenLogThenStop =>
Effect
.persist(Incremented(1), Incremented(2))
.thenRun { (_: State) =>
loggingActor ! firstLogging
}
.thenStop()
case IncrementWithPersistAll(n) =>
Effect.persist((0 until n).map(_ => Incremented(1)))
case IncrementWithConfirmation(replyTo) =>
Effect.persist(Incremented(1)).thenReply(replyTo)(_ => Done)
case GetValue(replyTo) =>
replyTo ! state
Effect.none
case IncrementLater =>
// purpose is to test signals
val delay = ctx.spawnAnonymous(Behaviors.withTimers[Tick.type] { timers =>
timers.startSingleTimer(Tick, 10.millis)
Behaviors.receive((_, msg) =>
msg match {
case Tick => Behaviors.stopped
})
})
ctx.watchWith(delay, DelayFinished)
Effect.none
case DelayFinished =>
Effect.persist(Incremented(10))
case IncrementAfterReceiveTimeout =>
ctx.setReceiveTimeout(10.millis, Timeout)
Effect.none
case Timeout =>
ctx.cancelReceiveTimeout()
Effect.persist(Incremented(100))
case IncrementTwiceAndThenLog =>
Effect
.persist(Incremented(1), Incremented(1))
.thenRun { (_: State) =>
loggingActor ! firstLogging
}
.thenRun { _ =>
loggingActor ! secondLogging
}
case EmptyEventsListAndThenLog =>
Effect
.persist(List.empty) // send empty list of events
.thenRun { _ =>
loggingActor ! firstLogging
}
case DoNothingAndThenLog =>
Effect.none.thenRun { _ =>
loggingActor ! firstLogging
}
case LogThenStop =>
Effect
.none[Event, State]
.thenRun { _ =>
loggingActor ! firstLogging
}
.thenStop()
case Fail =>
throw new TestException("boom!")
case StopIt =>
Effect.none.thenStop()
},
eventHandler = (state, evt) =>
evt match {
case Incremented(delta) =>
probe ! ((state, evt))
State(state.value + delta, state.history :+ state.value)
}).receiveSignal {
case (_, RecoveryCompleted) => ()
case (_, SnapshotCompleted(metadata)) =>
snapshotProbe ! Success(metadata)
case (_, SnapshotFailed(_, failure)) =>
snapshotProbe ! Failure(failure)
}
}
}
class EventSourcedBehaviorSpec
extends ScalaTestWithActorTestKit(EventSourcedBehaviorSpec.conf)
with AnyWordSpecLike
with LogCapturing {
import EventSourcedBehaviorSpec._
val queries: PersistenceTestKitReadJournal =
PersistenceQuery(system).readJournalFor[PersistenceTestKitReadJournal](PersistenceTestKitReadJournal.Identifier)
val pidCounter = new AtomicInteger(0)
private def nextPid(): PersistenceId = PersistenceId.ofUniqueId(s"c${pidCounter.incrementAndGet()})")
"A typed persistent actor" must {
"persist an event" in {
val c = spawn(counter(nextPid()))
val probe = TestProbe[State]()
c ! Increment
c ! GetValue(probe.ref)
probe.expectMessage(State(1, Vector(0)))
}
"replay stored events" in {
val pid = nextPid()
val c = spawn(counter(pid))
val probe = TestProbe[State]()
c ! Increment
c ! Increment
c ! Increment
c ! GetValue(probe.ref)
probe.expectMessage(10.seconds, State(3, Vector(0, 1, 2)))
val c2 = spawn(counter(pid))
c2 ! GetValue(probe.ref)
probe.expectMessage(State(3, Vector(0, 1, 2)))
c2 ! Increment
c2 ! GetValue(probe.ref)
probe.expectMessage(State(4, Vector(0, 1, 2, 3)))
}
"handle Terminated signal" in {
val c = spawn(counter(nextPid()))
val probe = TestProbe[State]()
c ! Increment
c ! IncrementLater
eventually {
c ! GetValue(probe.ref)
probe.expectMessage(State(11, Vector(0, 1)))
}
}
"handle receive timeout" in {
val c = spawn(counter(nextPid()))
val probe = TestProbe[State]()
c ! Increment
c ! IncrementAfterReceiveTimeout
// let it timeout
Thread.sleep(500)
eventually {
c ! GetValue(probe.ref)
probe.expectMessage(State(101, Vector(0, 1)))
}
}
"adhere default and disabled Recovery strategies" in {
val pid = nextPid()
val probe = TestProbe[State]()
def counterWithRecoveryStrategy(recoveryStrategy: Recovery) =
Behaviors.setup[Command](counter(_, pid).withRecovery(recoveryStrategy))
val counterSetup = spawn(counterWithRecoveryStrategy(Recovery.default))
counterSetup ! Increment
counterSetup ! Increment
counterSetup ! Increment
counterSetup ! GetValue(probe.ref)
probe.expectMessage(State(3, Vector(0, 1, 2)))
counterSetup ! Increment
counterSetup ! StopIt
probe.expectTerminated(counterSetup)
val counterDefaultRecoveryStrategy = spawn(counterWithRecoveryStrategy(Recovery.default))
counterDefaultRecoveryStrategy ! GetValue(probe.ref)
probe.expectMessage(State(4, Vector(0, 1, 2, 3)))
counterDefaultRecoveryStrategy ! StopIt
probe.expectTerminated(counterDefaultRecoveryStrategy)
val counterDisabledRecoveryStrategy = spawn(counterWithRecoveryStrategy(Recovery.disabled))
counterDisabledRecoveryStrategy ! Increment
counterDisabledRecoveryStrategy ! Increment
counterDisabledRecoveryStrategy ! GetValue(probe.ref)
probe.expectMessage(State(2, Vector(0, 1)))
}
"adhere Recovery strategy with SnapshotSelectionCriteria" in {
val pid = nextPid()
val eventProbe = TestProbe[(State, Event)]()
val commandProbe = TestProbe[State]()
val snapshotProbe = TestProbe[Try[SnapshotMetadata]]()
def counterWithSnapshotSelectionCriteria(recoveryStrategy: Recovery) =
Behaviors.setup[Command](
counterWithProbe(_, pid, eventProbe.ref, snapshotProbe.ref).withRecovery(recoveryStrategy).snapshotWhen {
case (_, _, _) => true
})
val counterSetup = spawn(counterWithSnapshotSelectionCriteria(Recovery.default))
counterSetup ! Increment
counterSetup ! Increment
counterSetup ! Increment
eventProbe.receiveMessages(3)
snapshotProbe.receiveMessages(3)
counterSetup ! GetValue(commandProbe.ref)
commandProbe.expectMessage(State(3, Vector(0, 1, 2)))
counterSetup ! StopIt
commandProbe.expectTerminated(counterSetup)
val counterWithSnapshotSelectionCriteriaNone = spawn(
counterWithSnapshotSelectionCriteria(Recovery.withSnapshotSelectionCriteria(SnapshotSelectionCriteria.none)))
// replay all events, no snapshot
eventProbe.expectMessage(State(0, Vector.empty) -> Incremented(1))
eventProbe.expectMessage(State(1, Vector(0)) -> Incremented(1))
eventProbe.expectMessage(State(2, Vector(0, 1)) -> Incremented(1))
counterWithSnapshotSelectionCriteriaNone ! Increment
eventProbe.expectMessage(State(3, Vector(0, 1, 2)) -> Incremented(1))
counterWithSnapshotSelectionCriteriaNone ! GetValue(commandProbe.ref)
commandProbe.expectMessage(State(4, Vector(0, 1, 2, 3)))
counterWithSnapshotSelectionCriteriaNone ! StopIt
commandProbe.expectTerminated(counterWithSnapshotSelectionCriteriaNone)
val counterWithSnapshotSelectionCriteriaLatest = spawn(
counterWithSnapshotSelectionCriteria(Recovery.withSnapshotSelectionCriteria(SnapshotSelectionCriteria.latest)))
// replay no events, only latest snapshot
eventProbe.expectNoMessage()
counterWithSnapshotSelectionCriteriaLatest ! Increment
counterWithSnapshotSelectionCriteriaLatest ! GetValue(commandProbe.ref)
commandProbe.expectMessage(State(5, Vector(0, 1, 2, 3, 4)))
}
/**
* Verify that all side-effects callbacks are called (in order) and only once.
* The [[IncrementTwiceAndThenLog]] command will emit two Increment events
*/
"chainable side effects with events" in {
val loggingProbe = TestProbe[String]()
val c = spawn(counter(nextPid(), loggingProbe.ref))
val probe = TestProbe[State]()
c ! IncrementTwiceAndThenLog
c ! GetValue(probe.ref)
probe.expectMessage(State(2, Vector(0, 1)))
loggingProbe.expectMessage(firstLogging)
loggingProbe.expectMessage(secondLogging)
}
"persist then stop" in {
val loggingProbe = TestProbe[String]()
val c = spawn(counter(nextPid(), loggingProbe.ref))
val watchProbe = watcher(c)
c ! IncrementThenLogThenStop
loggingProbe.expectMessage(firstLogging)
watchProbe.expectMessage("Terminated")
}
"persist(All) then stop" in {
val loggingProbe = TestProbe[String]()
val c = spawn(counter(nextPid(), loggingProbe.ref))
val watchProbe = watcher(c)
c ! IncrementTwiceThenLogThenStop
loggingProbe.expectMessage(firstLogging)
watchProbe.expectMessage("Terminated")
}
"persist an event thenReply" in {
val c = spawn(counter(nextPid()))
val probe = TestProbe[Done]()
c ! IncrementWithConfirmation(probe.ref)
probe.expectMessage(Done)
c ! IncrementWithConfirmation(probe.ref)
c ! IncrementWithConfirmation(probe.ref)
probe.expectMessage(Done)
probe.expectMessage(Done)
}
/** Proves that side-effects are called when emitting an empty list of events */
"chainable side effects without events" in {
val loggingProbe = TestProbe[String]()
val c = spawn(counter(nextPid(), loggingProbe.ref))
val probe = TestProbe[State]()
c ! EmptyEventsListAndThenLog
c ! GetValue(probe.ref)
probe.expectMessage(State(0, Vector.empty))
loggingProbe.expectMessage(firstLogging)
}
/** Proves that side-effects are called when explicitly calling Effect.none */
"chainable side effects when doing nothing (Effect.none)" in {
val loggingProbe = TestProbe[String]()
val c = spawn(counter(nextPid(), loggingProbe.ref))
val probe = TestProbe[State]()
c ! DoNothingAndThenLog
c ! GetValue(probe.ref)
probe.expectMessage(State(0, Vector.empty))
loggingProbe.expectMessage(firstLogging)
}
"work when wrapped in other behavior" in {
val probe = TestProbe[State]()
val behavior = Behaviors
.supervise[Command](counter(nextPid()))
.onFailure(SupervisorStrategy.restartWithBackoff(1.second, 10.seconds, 0.1))
val c = spawn(behavior)
c ! Increment
c ! GetValue(probe.ref)
probe.expectMessage(State(1, Vector(0)))
}
"stop after logging (no persisting)" in {
val loggingProbe = TestProbe[String]()
val c: ActorRef[Command] = spawn(counter(nextPid(), loggingProbe.ref))
val watchProbe = watcher(c)
c ! LogThenStop
loggingProbe.expectMessage(firstLogging)
watchProbe.expectMessage("Terminated")
}
"wrap persistent behavior in tap" in {
val probe = TestProbe[Command]()
val wrapped: Behavior[Command] = Behaviors.monitor(probe.ref, counter(nextPid()))
val c = spawn(wrapped)
c ! Increment
val replyProbe = TestProbe[State]()
c ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(1, Vector(0)))
probe.expectMessage(Increment)
}
"tag events" in {
val pid = nextPid()
val c = spawn(Behaviors.setup[Command](ctx => counter(ctx, pid).withTagger(_ => Set("tag1", "tag2"))))
val replyProbe = TestProbe[State]()
c ! Increment
c ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(1, Vector(0)))
val events = queries.currentEventsByTag("tag1", Offset.noOffset).runWith(Sink.seq).futureValue
events shouldEqual List(EventEnvelope(Sequence(1), pid.id, 1, Incremented(1), 0L))
}
"handle scheduled message arriving before recovery completed " in {
val c = spawn(Behaviors.withTimers[Command] { timers =>
timers.startSingleTimer(Increment, 1.millis)
Thread.sleep(30) // now it's probably already in the mailbox, and will be stashed
counter(nextPid())
})
val probe = TestProbe[State]()
c ! Increment
probe.awaitAssert {
c ! GetValue(probe.ref)
probe.expectMessage(State(2, Vector(0, 1)))
}
}
"handle scheduled message arriving after recovery completed " in {
val c = spawn(Behaviors.withTimers[Command] { timers =>
// probably arrives after recovery completed
timers.startSingleTimer(Increment, 200.millis)
counter(nextPid())
})
val probe = TestProbe[State]()
c ! Increment
probe.awaitAssert {
c ! GetValue(probe.ref)
probe.expectMessage(State(2, Vector(0, 1)))
}
}
"fail after recovery timeout" in {
LoggingTestKit.error("Exception during recovery from snapshot").expect {
val c = spawn(
Behaviors.setup[Command](ctx =>
counter(ctx, nextPid())
.withSnapshotPluginId("slow-snapshot-store")
.withJournalPluginId("short-recovery-timeout")))
val probe = TestProbe[State]()
probe.expectTerminated(c, probe.remainingOrDefault)
}
}
"not wrap a failure caused by command stashed while recovering in a journal failure" in {
val pid = nextPid()
val probe = TestProbe[AnyRef]()
// put some events in there, so that recovering takes a little time
val c = spawn(Behaviors.setup[Command](counter(_, pid)))
(0 to 50).foreach { _ =>
c ! IncrementWithConfirmation(probe.ref)
probe.expectMessage(Done)
}
c ! StopIt
probe.expectTerminated(c)
LoggingTestKit.error[TestException].expect {
val c2 = spawn(Behaviors.setup[Command](counter(_, pid)))
c2 ! Fail
probe.expectTerminated(c2) // should fail
}
}
"fail fast if persistenceId is null" in {
intercept[IllegalArgumentException] {
PersistenceId.ofUniqueId(null)
}
val probe = TestProbe[AnyRef]()
LoggingTestKit.error[ActorInitializationException].withMessageContains("persistenceId must not be null").expect {
val ref = spawn(Behaviors.setup[Command](counter(_, persistenceId = PersistenceId.ofUniqueId(null))))
probe.expectTerminated(ref)
}
LoggingTestKit.error[ActorInitializationException].withMessageContains("persistenceId must not be null").expect {
val ref = spawn(Behaviors.setup[Command](counter(_, persistenceId = null)))
probe.expectTerminated(ref)
}
}
"fail fast if persistenceId is empty" in {
intercept[IllegalArgumentException] {
PersistenceId.ofUniqueId("")
}
val probe = TestProbe[AnyRef]()
LoggingTestKit.error[ActorInitializationException].withMessageContains("persistenceId must not be empty").expect {
val ref = spawn(Behaviors.setup[Command](counter(_, persistenceId = PersistenceId.ofUniqueId(""))))
probe.expectTerminated(ref)
}
}
"fail fast if default journal plugin is not defined" in {
// new ActorSystem without persistence config
val testkit2 = ActorTestKit(ActorTestKitBase.testNameFromCallStack(), ConfigFactory.parseString(""))
try {
LoggingTestKit
.error[ActorInitializationException]
.withMessageContains("Default journal plugin is not configured")
.expect {
val ref = testkit2.spawn(Behaviors.setup[Command](counter(_, nextPid())))
val probe = testkit2.createTestProbe()
probe.expectTerminated(ref)
}(testkit2.system)
} finally {
testkit2.shutdownTestKit()
}
}
"fail fast if given journal plugin is not defined" in {
// new ActorSystem without persistence config
val testkit2 = ActorTestKit(ActorTestKitBase.testNameFromCallStack(), ConfigFactory.parseString(""))
try {
LoggingTestKit
.error[ActorInitializationException]
.withMessageContains("Journal plugin [missing] configuration doesn't exist")
.expect {
val ref = testkit2.spawn(Behaviors.setup[Command](counter(_, nextPid()).withJournalPluginId("missing")))
val probe = testkit2.createTestProbe()
probe.expectTerminated(ref)
}(testkit2.system)
} finally {
testkit2.shutdownTestKit()
}
}
"warn if default snapshot plugin is not defined" in {
// new ActorSystem without snapshot plugin config
val testkit2 = ActorTestKit(
ActorTestKitBase.testNameFromCallStack(),
ConfigFactory.parseString(s"""
pekko.persistence.journal.plugin = "pekko.persistence.journal.inmem"
"""))
try {
LoggingTestKit
.warn("No default snapshot store configured")
.expect {
val ref = testkit2.spawn(Behaviors.setup[Command](counter(_, nextPid())))
val probe = testkit2.createTestProbe[State]()
// verify that it's not terminated
ref ! GetValue(probe.ref)
probe.expectMessage(State(0, Vector.empty))
}(testkit2.system)
} finally {
testkit2.shutdownTestKit()
}
}
"fail fast if given snapshot plugin is not defined" in {
// new ActorSystem without snapshot plugin config
val testkit2 = ActorTestKit(
ActorTestKitBase.testNameFromCallStack(),
ConfigFactory.parseString(s"""
pekko.persistence.journal.plugin = "pekko.persistence.journal.inmem"
"""))
try {
LoggingTestKit
.error[ActorInitializationException]
.withMessageContains("Snapshot store plugin [missing] configuration doesn't exist")
.expect {
val ref = testkit2.spawn(Behaviors.setup[Command](counter(_, nextPid()).withSnapshotPluginId("missing")))
val probe = testkit2.createTestProbe()
probe.expectTerminated(ref)
}(testkit2.system)
} finally {
testkit2.shutdownTestKit()
}
}
"allow enumerating all ids" in {
val all = queries.currentPersistenceIds(None, Long.MaxValue).runWith(Sink.seq).futureValue
all.size should be > 5
val firstThree = queries.currentPersistenceIds(None, 3).runWith(Sink.seq).futureValue
firstThree.size shouldBe 3
val others = queries.currentPersistenceIds(Some(firstThree.last), Long.MaxValue).runWith(Sink.seq).futureValue
firstThree ++ others should contain theSameElementsInOrderAs all
}
def watcher(toWatch: ActorRef[_]): TestProbe[String] = {
val probe = TestProbe[String]()
val w = Behaviors.setup[Any] { ctx =>
ctx.watch(toWatch)
Behaviors
.receive[Any] { (_, _) =>
Behaviors.same
}
.receiveSignal {
case (_, _: Terminated) =>
probe.ref ! "Terminated"
Behaviors.stopped
}
}
spawn(w)
probe
}
}
}

View file

@ -0,0 +1,736 @@
/*
* Copyright (C) 2017-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.NotUsed
import pekko.actor.Dropped
import pekko.actor.UnhandledMessage
import pekko.actor.testkit.typed.TestException
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.PostStop
import pekko.actor.typed.SupervisorStrategy
import pekko.actor.typed.eventstream.EventStream
import pekko.actor.typed.internal.PoisonPill
import pekko.actor.typed.javadsl.StashOverflowException
import pekko.actor.typed.scaladsl.Behaviors
import pekko.actor.typed.scaladsl.adapter._
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.RecoveryCompleted
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import scala.concurrent.duration._
object EventSourcedBehaviorStashSpec {
def conf: Config = ConfigFactory.parseString(s"""
#pekko.loglevel = DEBUG
#pekko.persistence.typed.log-stashing = on
pekko.persistence.journal.plugin = "pekko.persistence.journal.inmem"
pekko.persistence.journal.plugin = "failure-journal"
# tune it down a bit so we can hit limit
pekko.persistence.typed.stash-capacity = 500
failure-journal = $${pekko.persistence.journal.inmem}
failure-journal {
class = "org.apache.pekko.persistence.typed.scaladsl.ChaosJournal"
}
""").withFallback(ConfigFactory.defaultReference()).resolve()
sealed trait Command[ReplyMessage]
// Unstash and change to active mode
final case class Activate(id: String, val replyTo: ActorRef[Ack]) extends Command[Ack]
// Change to active mode, stash incoming Increment
final case class Deactivate(id: String, val replyTo: ActorRef[Ack]) extends Command[Ack]
// Persist Incremented if in active mode, otherwise stashed
final case class Increment(id: String, val replyTo: ActorRef[Ack]) extends Command[Ack]
// Persist ValueUpdated, independent of active/inactive
final case class UpdateValue(id: String, value: Int, val replyTo: ActorRef[Ack]) extends Command[Ack]
// Retrieve current state, independent of active/inactive
final case class GetValue(replyTo: ActorRef[State]) extends Command[State]
final case class Unhandled(replyTo: ActorRef[NotUsed]) extends Command[NotUsed]
final case class Throw(id: String, t: Throwable, val replyTo: ActorRef[Ack]) extends Command[Ack]
final case class IncrementThenThrow(id: String, t: Throwable) extends Command[Ack]
final case class Slow(id: String, latch: CountDownLatch, val replyTo: ActorRef[Ack]) extends Command[Ack]
final case class Ack(id: String)
sealed trait Event
final case class Incremented(delta: Int) extends Event
final case class ValueUpdated(value: Int) extends Event
case object Activated extends Event
case object Deactivated extends Event
final case class State(value: Int, active: Boolean)
def counter(persistenceId: PersistenceId, signalProbe: Option[ActorRef[String]] = None): Behavior[Command[_]] =
Behaviors
.supervise[Command[_]] {
Behaviors.setup(_ => eventSourcedCounter(persistenceId, signalProbe))
}
.onFailure(SupervisorStrategy.restart.withLoggingEnabled(enabled = false))
def eventSourcedCounter(
persistenceId: PersistenceId,
signalProbe: Option[ActorRef[String]]): EventSourcedBehavior[Command[_], Event, State] = {
EventSourcedBehavior
.withEnforcedReplies[Command[_], Event, State](
persistenceId,
emptyState = State(0, active = true),
commandHandler = (state, command) => {
if (state.active) active(state, command)
else inactive(state, command)
},
eventHandler = (state, evt) =>
evt match {
case Incremented(delta) =>
if (!state.active) throw new IllegalStateException
State(state.value + delta, active = true)
case ValueUpdated(value) =>
State(value, active = state.active)
case Activated =>
if (state.active) throw new IllegalStateException
state.copy(active = true)
case Deactivated =>
if (!state.active) throw new IllegalStateException
state.copy(active = false)
})
.onPersistFailure(SupervisorStrategy
.restartWithBackoff(1.second, maxBackoff = 2.seconds, 0.0)
.withLoggingEnabled(enabled = false))
.receiveSignal {
case (state, RecoveryCompleted) => signalProbe.foreach(_ ! s"RecoveryCompleted-${state.value}")
case (_, PostStop) => signalProbe.foreach(_ ! "PostStop")
}
}
private def active(state: State, command: Command[_]): ReplyEffect[Event, State] = {
command match {
case Increment(id, replyTo) =>
Effect.persist(Incremented(1)).thenReply(replyTo)(_ => Ack(id))
case UpdateValue(id, value, replyTo) =>
Effect.persist(ValueUpdated(value)).thenReply(replyTo)(_ => Ack(id))
case GetValue(replyTo) =>
Effect.reply(replyTo)(state)
case Deactivate(id, replyTo) =>
Effect.persist(Deactivated).thenReply(replyTo)(_ => Ack(id))
case Activate(id, replyTo) =>
// already active
Effect.reply(replyTo)(Ack(id))
case _: Unhandled =>
Effect.unhandled.thenNoReply()
case Throw(id, t, replyTo) =>
replyTo ! Ack(id)
throw t
case IncrementThenThrow(_, throwable) =>
Effect.persist(Incremented(1)).thenRun((_: State) => throw throwable).thenNoReply()
case Slow(id, latch, replyTo) =>
latch.await(30, TimeUnit.SECONDS)
Effect.reply(replyTo)(Ack(id))
}
}
private def inactive(state: State, command: Command[_]): ReplyEffect[Event, State] = {
command match {
case _: Increment =>
Effect.stash()
case UpdateValue(id, value, replyTo) =>
Effect.persist(ValueUpdated(value)).thenReply(replyTo)(_ => Ack(id))
case GetValue(replyTo) =>
Effect.reply(replyTo)(state)
case Deactivate(id, replyTo) =>
// already inactive
Effect.reply(replyTo)(Ack(id))
case Activate(id, replyTo) =>
Effect.persist(Activated).thenReply(replyTo)((_: State) => Ack(id)).thenUnstashAll()
case _: Unhandled =>
Effect.unhandled.thenNoReply()
case Throw(id, t, replyTo) =>
replyTo ! Ack(id)
throw t
case _: IncrementThenThrow =>
Effect.stash()
case _: Slow =>
Effect.stash()
}
}
}
class EventSourcedBehaviorStashSpec
extends ScalaTestWithActorTestKit(EventSourcedBehaviorStashSpec.conf)
with AnyWordSpecLike
with LogCapturing {
import EventSourcedBehaviorStashSpec._
val pidCounter = new AtomicInteger(0)
private def nextPid(): PersistenceId = PersistenceId.ofUniqueId(s"c${pidCounter.incrementAndGet()})")
"A typed persistent actor that is stashing commands" must {
"stash and unstash" in {
val c = spawn(counter(nextPid()))
val ackProbe = TestProbe[Ack]()
val stateProbe = TestProbe[State]()
c ! Increment("1", ackProbe.ref)
ackProbe.expectMessage(Ack("1"))
c ! Deactivate("2", ackProbe.ref)
ackProbe.expectMessage(Ack("2"))
c ! Increment("3", ackProbe.ref)
c ! Increment("4", ackProbe.ref)
c ! GetValue(stateProbe.ref)
stateProbe.expectMessage(State(1, active = false))
c ! Activate("5", ackProbe.ref)
c ! Increment("6", ackProbe.ref)
ackProbe.expectMessage(Ack("5"))
ackProbe.expectMessage(Ack("3"))
ackProbe.expectMessage(Ack("4"))
ackProbe.expectMessage(Ack("6"))
c ! GetValue(stateProbe.ref)
stateProbe.expectMessage(State(4, active = true))
}
"handle mix of stash, persist and unstash" in {
val c = spawn(counter(nextPid()))
val ackProbe = TestProbe[Ack]()
val stateProbe = TestProbe[State]()
c ! Increment("1", ackProbe.ref)
ackProbe.expectMessage(Ack("1"))
c ! Deactivate("2", ackProbe.ref)
ackProbe.expectMessage(Ack("2"))
c ! Increment("3", ackProbe.ref)
c ! Increment("4", ackProbe.ref)
// UpdateValue will persist when inactive (with previously stashed commands)
c ! UpdateValue("5", 100, ackProbe.ref)
ackProbe.expectMessage(Ack("5"))
c ! GetValue(stateProbe.ref)
stateProbe.expectMessage(State(100, active = false))
c ! Activate("6", ackProbe.ref)
c ! GetValue(stateProbe.ref)
stateProbe.expectMessage(State(102, active = true))
ackProbe.expectMessage(Ack("6"))
ackProbe.expectMessage(Ack("3"))
ackProbe.expectMessage(Ack("4"))
}
"unstash in right order" in {
val c = spawn(counter(nextPid()))
val ackProbe = TestProbe[Ack]()
val stateProbe = TestProbe[State]()
c ! Increment(s"inc-1", ackProbe.ref)
c ! Deactivate(s"deact", ackProbe.ref)
c ! Increment(s"inc-2", ackProbe.ref)
c ! Increment(s"inc-3", ackProbe.ref)
c ! Increment(s"inc-4", ackProbe.ref)
c ! Activate(s"act", ackProbe.ref)
c ! Increment(s"inc-5", ackProbe.ref)
c ! Increment(s"inc-6", ackProbe.ref)
c ! Increment(s"inc-7", ackProbe.ref)
c ! GetValue(stateProbe.ref)
val finalState = stateProbe.expectMessageType[State](5.seconds)
// verify the order
ackProbe.expectMessage(Ack("inc-1"))
ackProbe.expectMessage(Ack("deact"))
ackProbe.expectMessage(Ack("act"))
ackProbe.expectMessage(Ack("inc-2"))
ackProbe.expectMessage(Ack("inc-3"))
ackProbe.expectMessage(Ack("inc-4"))
ackProbe.expectMessage(Ack("inc-5"))
ackProbe.expectMessage(Ack("inc-6"))
ackProbe.expectMessage(Ack("inc-7"))
finalState.value should ===(7)
}
"handle many stashed" in {
val c = spawn(counter(nextPid()))
val ackProbe = TestProbe[Ack]()
val stateProbe = TestProbe[State]()
val notUsedProbe = TestProbe[NotUsed]()
val unhandledProbe = createTestProbe[UnhandledMessage]()
system.eventStream ! EventStream.Subscribe(unhandledProbe.ref)
(1 to 100).foreach { n =>
c ! Increment(s"inc-1-$n", ackProbe.ref)
}
(1 to 3).foreach { n =>
c ! Deactivate(s"deact-2-$n", ackProbe.ref)
}
(1 to 100).foreach { n =>
if (n % 10 == 0)
c ! Unhandled(notUsedProbe.ref)
c ! Increment(s"inc-3-$n", ackProbe.ref)
}
c ! GetValue(stateProbe.ref)
(1 to 5).foreach { n =>
c ! UpdateValue(s"upd-4-$n", n * 1000, ackProbe.ref)
}
(1 to 3).foreach { n =>
c ! Activate(s"act-5-$n", ackProbe.ref)
}
(1 to 100).foreach { n =>
c ! Increment(s"inc-6-$n", ackProbe.ref)
}
c ! GetValue(stateProbe.ref)
(6 to 8).foreach { n =>
c ! UpdateValue(s"upd-7-$n", n * 1000, ackProbe.ref)
}
(1 to 3).foreach { n =>
c ! Deactivate(s"deact-8-$n", ackProbe.ref)
}
(1 to 100).foreach { n =>
c ! Increment(s"inc-9-$n", ackProbe.ref)
}
(1 to 3).foreach { n =>
c ! Activate(s"act-10-$n", ackProbe.ref)
}
c ! GetValue(stateProbe.ref)
unhandledProbe.receiveMessages(10)
val value1 = stateProbe.expectMessageType[State](5.seconds).value
val value2 = stateProbe.expectMessageType[State](5.seconds).value
val value3 = stateProbe.expectMessageType[State](5.seconds).value
// verify the order
(1 to 100).foreach { n =>
ackProbe.expectMessage(Ack(s"inc-1-$n"))
}
(1 to 3).foreach { n =>
ackProbe.expectMessage(Ack(s"deact-2-$n"))
}
(1 to 5).foreach { n =>
ackProbe.expectMessage(Ack(s"upd-4-$n"))
}
ackProbe.expectMessage(Ack("act-5-1"))
(1 to 100).foreach { n =>
ackProbe.expectMessage(Ack(s"inc-3-$n"))
}
(2 to 3).foreach { n =>
ackProbe.expectMessage(Ack(s"act-5-$n"))
}
(1 to 100).foreach { n =>
ackProbe.expectMessage(Ack(s"inc-6-$n"))
}
(6 to 8).foreach { n =>
ackProbe.expectMessage(Ack(s"upd-7-$n"))
}
(1 to 3).foreach { n =>
ackProbe.expectMessage(Ack(s"deact-8-$n"))
}
ackProbe.expectMessage(Ack("act-10-1"))
(1 to 100).foreach { n =>
ackProbe.expectMessage(Ack(s"inc-9-$n"))
}
(2 to 3).foreach { n =>
ackProbe.expectMessage(Ack(s"act-10-$n"))
}
value1 should ===(100)
value2 should ===(5200)
value3 should ===(8100)
}
"discard user stash when restarted due to thrown exception" in {
val c = spawn(counter(nextPid()))
val ackProbe = TestProbe[Ack]()
val stateProbe = TestProbe[State]()
c ! Increment("inc-1", ackProbe.ref)
ackProbe.expectMessage(Ack("inc-1"))
c ! Deactivate("deact", ackProbe.ref)
ackProbe.expectMessage(Ack("deact"))
c ! Increment("inc-2", ackProbe.ref)
c ! Increment("inc-3", ackProbe.ref)
c ! GetValue(stateProbe.ref)
stateProbe.expectMessage(State(1, active = false))
c ! Throw("throw", new TestException("test"), ackProbe.ref)
ackProbe.expectMessage(Ack("throw"))
c ! Increment("inc-4", ackProbe.ref)
c ! GetValue(stateProbe.ref)
stateProbe.expectMessage(State(1, active = false))
c ! Activate("act", ackProbe.ref)
c ! Increment("inc-5", ackProbe.ref)
ackProbe.expectMessage(Ack("act"))
// inc-2 an inc-3 was in user stash, and didn't survive restart, as expected
ackProbe.expectMessage(Ack("inc-4"))
ackProbe.expectMessage(Ack("inc-5"))
c ! GetValue(stateProbe.ref)
stateProbe.expectMessage(State(3, active = true))
}
"discard internal stash when restarted due to thrown exception" in {
val c = spawn(counter(nextPid()))
val ackProbe = TestProbe[Ack]()
val stateProbe = TestProbe[State]()
val latch = new CountDownLatch(1)
// make first command slow to ensure that all subsequent commands are enqueued first
c ! Slow("slow", latch, ackProbe.ref)
(1 to 10).foreach { n =>
if (n == 3)
c ! IncrementThenThrow(s"inc-$n", new TestException("test"))
else
c ! Increment(s"inc-$n", ackProbe.ref)
}
latch.countDown()
ackProbe.expectMessage(Ack("slow"))
ackProbe.expectMessage(Ack("inc-1"))
ackProbe.expectMessage(Ack("inc-2"))
ackProbe.expectNoMessage()
c ! Increment("inc-11", ackProbe.ref)
ackProbe.expectMessage(Ack("inc-11"))
c ! GetValue(stateProbe.ref)
stateProbe.expectMessage(State(4, active = true))
}
"preserve internal stash when persist failed" in {
val c = spawn(counter(PersistenceId.ofUniqueId("fail-fifth-a")))
val ackProbe = TestProbe[Ack]()
val stateProbe = TestProbe[State]()
(1 to 10).foreach { n =>
c ! Increment(s"inc-$n", ackProbe.ref)
}
(1 to 10).foreach { n =>
if (n != 5)
ackProbe.expectMessage(Ack(s"inc-$n"))
}
c ! GetValue(stateProbe.ref)
stateProbe.expectMessage(State(9, active = true))
}
"unstash messages after stashed GetState" in {
import pekko.persistence.typed.internal.EventSourcedBehaviorImpl.{ GetState, GetStateReply }
val c = spawn(counter(nextPid()))
val ackProbe = TestProbe[Ack]()
val stateProbe = TestProbe[State]()
val getStateProbe = TestProbe[GetStateReply[State]]()
val latch = new CountDownLatch(1)
c ! Increment(s"inc-1", ackProbe.ref)
ackProbe.expectMessage(Ack(s"inc-1"))
// make sure all the following messages are already in the mailbox
c ! Slow("slow", latch, ackProbe.ref)
// this will persist an event
c ! Increment(s"inc-2", ackProbe.ref)
// while the next two messages are already in the mailbox, so they will be stashed in the persisting state
c.unsafeUpcast[Any] ! GetState(getStateProbe.ref)
c ! Increment(s"inc-3", ackProbe.ref)
c ! Increment(s"inc-4", ackProbe.ref)
latch.countDown()
ackProbe.expectMessage(Ack("slow"))
ackProbe.expectMessage(Ack(s"inc-2"))
getStateProbe.expectMessageType[GetStateReply[State]]
ackProbe.expectMessage(Ack(s"inc-3"))
ackProbe.expectMessage(Ack(s"inc-4"))
c ! GetValue(stateProbe.ref)
stateProbe.expectMessage(State(4, active = true))
}
"preserve user stash when persist failed" in {
val c = spawn(counter(PersistenceId.ofUniqueId("fail-fifth-b")))
val ackProbe = TestProbe[Ack]()
val stateProbe = TestProbe[State]()
c ! Increment("inc-1", ackProbe.ref)
ackProbe.expectMessage(Ack("inc-1"))
c ! Deactivate("deact", ackProbe.ref)
ackProbe.expectMessage(Ack("deact"))
c ! Increment("inc-2", ackProbe.ref)
c ! Increment("inc-3", ackProbe.ref) // this will fail when unstashed
c ! Increment("inc-4", ackProbe.ref)
c ! Increment("inc-5", ackProbe.ref)
c ! GetValue(stateProbe.ref)
stateProbe.expectMessage(State(1, active = false))
c ! Activate("act", ackProbe.ref)
c ! Increment("inc-6", ackProbe.ref)
ackProbe.expectMessage(Ack("act"))
ackProbe.expectMessage(Ack("inc-2"))
// inc-3 failed
// inc-4, inc-5 still in user stash and processed due to UnstashAll that was in progress
ackProbe.expectMessage(Ack("inc-4"))
ackProbe.expectMessage(Ack("inc-5"))
ackProbe.expectMessage(Ack("inc-6"))
c ! GetValue(stateProbe.ref)
stateProbe.expectMessage(State(5, active = true))
}
"discard when stash has reached limit with default dropped setting" in {
val probe = TestProbe[AnyRef]()
system.toClassic.eventStream.subscribe(probe.ref.toClassic, classOf[Dropped])
val behavior = Behaviors.setup[String] { context =>
EventSourcedBehavior[String, String, Boolean](
persistenceId = PersistenceId.ofUniqueId("stash-is-full-drop"),
emptyState = false,
commandHandler = { (state, command) =>
state match {
case false =>
command match {
case "ping" =>
probe.ref ! "pong"
Effect.none
case "start-stashing" =>
Effect.persist("start-stashing")
case msg =>
probe.ref ! msg
Effect.none
}
case true =>
command match {
case "unstash" =>
Effect.persist("unstash").thenRun((_: Boolean) => context.self ! "done-unstashing").thenUnstashAll()
case _ =>
Effect.stash()
}
}
},
{
case (_, "start-stashing") => true
case (_, "unstash") => false
case (_, _) => throw new IllegalArgumentException()
})
}
val c = spawn(behavior)
// make sure it completed recovery, before we try to overfill the stash
c ! "ping"
probe.expectMessage("pong")
c ! "start-stashing"
val limit = system.settings.config.getInt("pekko.persistence.typed.stash-capacity")
LoggingTestKit.warn("Stash buffer is full, dropping message").expect {
(0 to limit).foreach { n =>
c ! s"cmd-$n" // limit triggers overflow
}
probe.expectMessageType[Dropped]
}
// we can still unstash and continue interacting
c ! "unstash"
(0 until limit).foreach { n =>
probe.expectMessage(s"cmd-$n")
}
probe.expectMessage("done-unstashing") // before actually unstashing, see above
c ! "ping"
probe.expectMessage("pong")
}
"fail when stash has reached limit if configured to fail" in {
// persistence settings is system wide, so we need to have a custom testkit/actorsystem here
val failStashTestKit = ActorTestKit(
"EventSourcedBehaviorStashSpec-stash-overflow-fail",
ConfigFactory
.parseString("pekko.persistence.typed.stash-overflow-strategy=fail")
.withFallback(EventSourcedBehaviorStashSpec.conf))
try {
val probe = failStashTestKit.createTestProbe[AnyRef]()
val behavior =
EventSourcedBehavior[String, String, String](
PersistenceId.ofUniqueId("stash-is-full-fail"),
"",
commandHandler = {
case (_, "ping") =>
probe.ref ! "pong"
Effect.none
case (_, _) =>
Effect.stash()
},
(state, _) => state)
val c = failStashTestKit.spawn(behavior)
// make sure recovery completed
c ! "ping"
probe.expectMessage("pong")
LoggingTestKit
.error[StashOverflowException]
.expect {
val limit = system.settings.config.getInt("pekko.persistence.typed.stash-capacity")
(0 to limit).foreach { n =>
c ! s"cmd-$n" // limit triggers overflow
}
probe.expectTerminated(c, 10.seconds)
}(failStashTestKit.system)
} finally {
failStashTestKit.shutdownTestKit()
}
}
"stop from PoisonPill even though user stash is not empty" in {
val c = spawn(counter(nextPid()))
val ackProbe = TestProbe[Ack]()
c ! Increment("1", ackProbe.ref)
ackProbe.expectMessage(Ack("1"))
c ! Deactivate("2", ackProbe.ref)
ackProbe.expectMessage(Ack("2"))
// stash 3 and 4
c ! Increment("3", ackProbe.ref)
c ! Increment("4", ackProbe.ref)
c.toClassic ! PoisonPill
ackProbe.expectTerminated(c)
}
"stop from PoisonPill after unstashing completed" in {
val c = spawn(counter(nextPid()))
val ackProbe = TestProbe[Ack]()
c ! Increment("1", ackProbe.ref)
ackProbe.expectMessage(Ack("1"))
c ! Deactivate("2", ackProbe.ref)
ackProbe.expectMessage(Ack("2"))
// stash 3 and 4
c ! Increment("3", ackProbe.ref)
c ! Increment("4", ackProbe.ref)
// start unstashing
c ! Activate("5", ackProbe.ref)
c.toClassic ! PoisonPill
// 6 shouldn't make it, already stopped
c ! Increment("6", ackProbe.ref)
ackProbe.expectMessage(Ack("5"))
// not stopped before 3 and 4 were processed
ackProbe.expectMessage(Ack("3"))
ackProbe.expectMessage(Ack("4"))
ackProbe.expectTerminated(c)
// 6 shouldn't make it, already stopped
ackProbe.expectNoMessage(100.millis)
}
"stop from PoisonPill after recovery completed" in {
val pid = nextPid()
val c = spawn(counter(pid))
val ackProbe = TestProbe[Ack]()
c ! Increment("1", ackProbe.ref)
c ! Increment("2", ackProbe.ref)
c ! Increment("3", ackProbe.ref)
ackProbe.expectMessage(Ack("1"))
ackProbe.expectMessage(Ack("2"))
ackProbe.expectMessage(Ack("3"))
val signalProbe = TestProbe[String]()
val c2 = spawn(counter(pid, Some(signalProbe.ref)))
// this PoisonPill will most likely be received in RequestingRecoveryPermit since it's sent immediately
c2.toClassic ! PoisonPill
signalProbe.expectMessage("RecoveryCompleted-3")
signalProbe.expectMessage("PostStop")
val c3 = spawn(counter(pid, Some(signalProbe.ref)))
// this PoisonPill will most likely be received in RequestingRecoveryPermit since it's sent slightly afterwards
Thread.sleep(1)
c3.toClassic ! PoisonPill
c3 ! Increment("4", ackProbe.ref)
signalProbe.expectMessage("RecoveryCompleted-3")
signalProbe.expectMessage("PostStop")
ackProbe.expectNoMessage(20.millis)
val c4 = spawn(counter(pid, Some(signalProbe.ref)))
signalProbe.expectMessage("RecoveryCompleted-3")
// this PoisonPill will be received in Running
c4.toClassic ! PoisonPill
c4 ! Increment("4", ackProbe.ref)
signalProbe.expectMessage("PostStop")
ackProbe.expectNoMessage(20.millis)
val c5 = spawn(counter(pid, Some(signalProbe.ref)))
signalProbe.expectMessage("RecoveryCompleted-3")
c5 ! Increment("4", ackProbe.ref)
c5 ! Increment("5", ackProbe.ref)
// this PoisonPill will most likely be received in PersistingEvents
c5.toClassic ! PoisonPill
c5 ! Increment("6", ackProbe.ref)
ackProbe.expectMessage(Ack("4"))
ackProbe.expectMessage(Ack("5"))
signalProbe.expectMessage("PostStop")
ackProbe.expectNoMessage(20.millis)
}
}
}

View file

@ -0,0 +1,116 @@
/*
* Copyright (C) 2017-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.typed.PersistenceId
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.concurrent.atomic.AtomicInteger
import scala.concurrent.duration._
object EventSourcedBehaviorTimersSpec {
val journalId = "event-sourced-behavior-timers-spec"
def testBehavior(persistenceId: PersistenceId, probe: ActorRef[String]): Behavior[String] =
Behaviors.setup { _ =>
Behaviors.withTimers { timers =>
EventSourcedBehavior[String, String, String](
persistenceId,
emptyState = "",
commandHandler = (_, command) =>
command match {
case "scheduled" =>
probe ! "scheduled"
Effect.none
case "cmd-0" =>
timers.startSingleTimer("key", "scheduled", Duration.Zero)
Effect.none
case _ =>
timers.startSingleTimer("key", "scheduled", Duration.Zero)
Effect.persist(command).thenRun(_ => probe ! command)
},
eventHandler = (state, evt) => state + evt)
}
}
def testTimerFromSetupBehavior(persistenceId: PersistenceId, probe: ActorRef[String]): Behavior[String] =
Behaviors.setup { _ =>
Behaviors.withTimers { timers =>
timers.startSingleTimer("key", "scheduled", Duration.Zero)
EventSourcedBehavior[String, String, String](
persistenceId,
emptyState = "",
commandHandler = (_, command) =>
command match {
case "scheduled" =>
probe ! "scheduled"
Effect.none
case _ =>
Effect.persist(command).thenRun(_ => probe ! command)
},
eventHandler = (state, evt) => state + evt)
}
}
}
class EventSourcedBehaviorTimersSpec
extends ScalaTestWithActorTestKit(PersistenceTestKitPlugin.config)
with AnyWordSpecLike
with LogCapturing {
import EventSourcedBehaviorTimersSpec._
val pidCounter = new AtomicInteger(0)
private def nextPid(): PersistenceId = PersistenceId.ofUniqueId(s"c${pidCounter.incrementAndGet()})")
"EventSourcedBehavior withTimers" must {
"be able to schedule message" in {
val probe = createTestProbe[String]()
val pid = nextPid()
val ref = spawn(testBehavior(pid, probe.ref))
ref ! "cmd-0"
probe.expectMessage("scheduled")
}
"not discard timer msg due to stashing" in {
val probe = createTestProbe[String]()
val pid = nextPid()
val ref = spawn(testBehavior(pid, probe.ref))
ref ! "cmd-1"
probe.expectMessage("cmd-1")
probe.expectMessage("scheduled")
}
"be able to schedule message from setup" in {
val probe = createTestProbe[String]()
val pid = nextPid()
val ref = spawn(testTimerFromSetupBehavior(pid, probe.ref))
probe.expectMessage("scheduled")
(1 to 20).foreach { n =>
ref ! s"cmd-$n"
}
probe.receiveMessages(20)
// start new instance that is likely to stash the timer message while replaying
spawn(testTimerFromSetupBehavior(pid, probe.ref))
probe.expectMessage("scheduled")
}
}
}

View file

@ -0,0 +1,213 @@
/*
* Copyright (C) 2019-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.TestException
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.LoggingTestKit
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.testkit.typed.scaladsl.TestProbe
import pekko.actor.typed._
import pekko.actor.typed.scaladsl.ActorContext
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.typed.internal.EventSourcedBehaviorImpl.WriterIdentity
import pekko.persistence.typed.internal.BehaviorSetup
import pekko.persistence.typed.internal.EventSourcedSettings
import pekko.persistence.typed.internal.InternalProtocol
import pekko.persistence.typed.internal.NoOpSnapshotAdapter
import pekko.persistence.typed.internal.StashState
import pekko.persistence.typed.NoOpEventAdapter
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.RecoveryCompleted
import pekko.persistence.{ Recovery => ClassicRecovery }
import pekko.serialization.jackson.CborSerializable
import pekko.util.ConstantFun
import org.scalatest.wordspec.AnyWordSpecLike
import org.slf4j.LoggerFactory
import java.util.concurrent.atomic.AtomicInteger
object EventSourcedBehaviorWatchSpec {
sealed trait Command extends CborSerializable
case object Fail extends Command
case object Stop extends Command
final case class ChildHasFailed(t: pekko.actor.typed.ChildFailed)
final case class HasTerminated(ref: ActorRef[_])
}
class EventSourcedBehaviorWatchSpec
extends ScalaTestWithActorTestKit(EventSourcedBehaviorSpec.conf)
with AnyWordSpecLike
with LogCapturing {
import EventSourcedBehaviorWatchSpec._
private val cause = TestException("Dodge this.")
private val pidCounter = new AtomicInteger(0)
private def nextPid: PersistenceId = PersistenceId.ofUniqueId(s"${pidCounter.incrementAndGet()}")
private val logger = LoggerFactory.getLogger(this.getClass)
private def setup(
pf: PartialFunction[(String, Signal), Unit],
settings: EventSourcedSettings,
context: ActorContext[_]): BehaviorSetup[Command, String, String] =
new BehaviorSetup[Command, String, String](
context.asInstanceOf[ActorContext[InternalProtocol]],
nextPid,
emptyState = "",
commandHandler = (_, _) => Effect.none,
eventHandler = (state, evt) => state + evt,
WriterIdentity.newIdentity(),
pf,
_ => Set.empty[String],
NoOpEventAdapter.instance[String],
NoOpSnapshotAdapter.instance[String],
snapshotWhen = ConstantFun.scalaAnyThreeToFalse,
ClassicRecovery(),
RetentionCriteria.disabled,
holdingRecoveryPermit = false,
settings = settings,
stashState = new StashState(context.asInstanceOf[ActorContext[InternalProtocol]], settings),
replication = None,
publishEvents = false,
internalLoggerFactory = () => logger)
"A typed persistent parent actor watching a child" must {
"throw a DeathPactException from parent when not handling the child Terminated signal" in {
val parent =
spawn(Behaviors.setup[Command] { context =>
val child = context.spawnAnonymous(Behaviors.receive[Command] { (_, _) =>
throw cause
})
context.watch(child)
EventSourcedBehavior[Command, String, String](nextPid, emptyState = "",
commandHandler = (_, cmd) => {
child ! cmd
Effect.none
}, eventHandler = (state, evt) => state + evt)
})
LoggingTestKit.error[TestException].expect {
LoggingTestKit.error[DeathPactException].expect {
parent ! Fail
}
}
createTestProbe().expectTerminated(parent)
}
"behave as expected if a user's signal handler is side effecting" in {
val signalHandler: PartialFunction[(String, Signal), Unit] = {
case (_, RecoveryCompleted) =>
java.time.Instant.now.getNano
Behaviors.same
}
Behaviors.setup[Command] { context =>
val settings = EventSourcedSettings(context.system, "", "")
setup(signalHandler, settings, context).onSignal("", RecoveryCompleted, false) shouldEqual true
setup(PartialFunction.empty, settings, context).onSignal("", RecoveryCompleted, false) shouldEqual false
Behaviors.empty
}
val parent =
spawn(Behaviors.setup[Command] { context =>
val child = context.spawnAnonymous(Behaviors.receive[Command] { (_, _) =>
throw cause
})
context.watch(child)
EventSourcedBehavior[Command, String, String](nextPid, emptyState = "",
commandHandler = (_, cmd) => {
child ! cmd
Effect.none
}, eventHandler = (state, evt) => state + evt).receiveSignal(signalHandler)
})
LoggingTestKit.error[TestException].expect {
LoggingTestKit.error[DeathPactException].expect {
parent ! Fail
}
}
createTestProbe().expectTerminated(parent)
}
"receive a Terminated when handling the signal" in {
val probe = TestProbe[AnyRef]()
val parent =
spawn(Behaviors.setup[Stop.type] { context =>
val child = context.spawnAnonymous(Behaviors.setup[Stop.type] { c =>
Behaviors.receive[Stop.type] { (_, _) =>
context.stop(c.self)
Behaviors.stopped
}
})
probe.ref ! child
context.watch(child)
EventSourcedBehavior[Stop.type, String, String](nextPid, emptyState = "",
commandHandler = (_, cmd) => {
child ! cmd
Effect.none
}, eventHandler = (state, evt) => state + evt).receiveSignal {
case (_, t: Terminated) =>
probe.ref ! HasTerminated(t.ref)
Behaviors.stopped
}
})
val child = probe.expectMessageType[ActorRef[Stop.type]]
parent ! Stop
probe.expectMessageType[HasTerminated].ref shouldEqual child
}
"receive a ChildFailed when handling the signal" in {
val probe = TestProbe[AnyRef]()
val parent =
spawn(Behaviors.setup[Fail.type] { context =>
val child = context.spawnAnonymous(Behaviors.receive[Fail.type] { (_, _) =>
throw cause
})
probe.ref ! child
context.watch(child)
EventSourcedBehavior[Fail.type, String, String](nextPid, emptyState = "",
commandHandler = (_, cmd) => {
child ! cmd
Effect.none
}, eventHandler = (state, evt) => state + evt).receiveSignal {
case (_, t: ChildFailed) =>
probe.ref ! ChildHasFailed(t)
Behaviors.same
}
})
val child = probe.expectMessageType[ActorRef[Fail.type]]
LoggingTestKit.error[TestException].expect {
parent ! Fail
}
val failed = probe.expectMessageType[ChildHasFailed].t
failed.ref shouldEqual child
failed.cause shouldEqual cause
}
}
}

View file

@ -0,0 +1,236 @@
/*
* Copyright (C) 2019-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.testkit.typed.scaladsl.TestProbe
import pekko.actor.typed.ActorRef
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.query.EventEnvelope
import pekko.persistence.query.PersistenceQuery
import pekko.persistence.query.Sequence
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.EventAdapter
import pekko.persistence.typed.EventSeq
import pekko.persistence.typed.PersistenceId
import pekko.serialization.jackson.CborSerializable
import pekko.stream.scaladsl.Sink
import pekko.testkit.JavaSerializable
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.concurrent.atomic.AtomicInteger
object EventSourcedEventAdapterSpec {
case class Wrapper(event: String) extends CborSerializable
class WrapperEventAdapter extends EventAdapter[String, Wrapper] {
override def toJournal(e: String): Wrapper = Wrapper("<" + e)
override def fromJournal(p: Wrapper, manifest: String): EventSeq[String] = EventSeq.single(p.event + ">")
override def manifest(event: String): String = ""
}
class FilterEventAdapter extends EventAdapter[String, String] {
override def toJournal(e: String): String = e.toUpperCase()
override def fromJournal(p: String, manifest: String): EventSeq[String] = {
if (p == "B") EventSeq.empty
else EventSeq.single(p)
}
override def manifest(event: String): String = ""
}
class SplitEventAdapter extends EventAdapter[String, String] {
override def toJournal(e: String): String = e.toUpperCase()
override def fromJournal(p: String, manifest: String): EventSeq[String] = {
EventSeq(p.map("<" + _.toString + ">"))
}
override def manifest(event: String): String = ""
}
class EventAdapterWithManifest extends EventAdapter[String, String] {
override def toJournal(e: String): String = e.toUpperCase()
override def fromJournal(p: String, manifest: String): EventSeq[String] = {
EventSeq.single(p + manifest)
}
override def manifest(event: String): String = event.length.toString
}
// generics doesn't work with Jackson, so using Java serialization
case class GenericWrapper[T](event: T) extends JavaSerializable
class GenericWrapperEventAdapter[T] extends EventAdapter[T, GenericWrapper[T]] {
override def toJournal(e: T): GenericWrapper[T] = GenericWrapper(e)
override def fromJournal(p: GenericWrapper[T], manifest: String): EventSeq[T] = EventSeq.single(p.event)
override def manifest(event: T): String = ""
}
}
class EventSourcedEventAdapterSpec
extends ScalaTestWithActorTestKit(ConfigFactory.parseString("""
pekko.persistence.testkit.events.serialize = true""").withFallback(PersistenceTestKitPlugin.config))
with AnyWordSpecLike
with LogCapturing {
import EventSourcedBehaviorSpec._
import EventSourcedEventAdapterSpec._
val pidCounter = new AtomicInteger(0)
private def nextPid(): PersistenceId = PersistenceId.ofUniqueId(s"c${pidCounter.incrementAndGet()})")
val queries: PersistenceTestKitReadJournal =
PersistenceQuery(system).readJournalFor[PersistenceTestKitReadJournal](PersistenceTestKitReadJournal.Identifier)
private def behavior(pid: PersistenceId, probe: ActorRef[String]): EventSourcedBehavior[String, String, String] =
EventSourcedBehavior(pid, "",
commandHandler = { (_, command) =>
Effect.persist(command).thenRun(newState => probe ! newState)
},
eventHandler = { (state, evt) =>
state + evt
})
"Event adapter" must {
"wrap single events" in {
val probe = TestProbe[String]()
val pid = nextPid()
val ref = spawn(behavior(pid, probe.ref).eventAdapter(new WrapperEventAdapter))
ref ! "a"
ref ! "b"
probe.expectMessage("a")
probe.expectMessage("ab")
// replay
val ref2 = spawn(behavior(pid, probe.ref).eventAdapter(new WrapperEventAdapter))
ref2 ! "c"
probe.expectMessage("<a><b>c")
}
"filter unused events" in {
val probe = TestProbe[String]()
val pid = nextPid()
val ref = spawn(behavior(pid, probe.ref).eventAdapter(new FilterEventAdapter))
ref ! "a"
ref ! "b"
ref ! "c"
probe.expectMessage("a")
probe.expectMessage("ab")
probe.expectMessage("abc")
// replay
val ref2 = spawn(behavior(pid, probe.ref).eventAdapter(new FilterEventAdapter))
ref2 ! "d"
probe.expectMessage("ACd")
}
"split one event into several" in {
val probe = TestProbe[String]()
val pid = nextPid()
val ref = spawn(behavior(pid, probe.ref).eventAdapter(new SplitEventAdapter))
ref ! "a"
ref ! "bc"
probe.expectMessage("a")
probe.expectMessage("abc")
// replay
val ref2 = spawn(behavior(pid, probe.ref).eventAdapter(new SplitEventAdapter))
ref2 ! "d"
probe.expectMessage("<A><B><C>d")
}
"support manifest" in {
val probe = TestProbe[String]()
val pid = nextPid()
val ref = spawn(behavior(pid, probe.ref).eventAdapter(new EventAdapterWithManifest))
ref ! "a"
ref ! "bcd"
probe.expectMessage("a")
probe.expectMessage("abcd")
// replay
val ref2 = spawn(behavior(pid, probe.ref).eventAdapter(new EventAdapterWithManifest))
ref2 ! "e"
probe.expectMessage("A1BCD3e")
}
"adapt events" in {
val pid = nextPid()
val c = spawn(Behaviors.setup[Command] { ctx =>
val persistentBehavior = counter(ctx, pid)
persistentBehavior.eventAdapter(new GenericWrapperEventAdapter[Event])
})
val replyProbe = TestProbe[State]()
c ! Increment
c ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(1, Vector(0)))
val events = queries.currentEventsByPersistenceId(pid.id).runWith(Sink.seq).futureValue
events shouldEqual List(EventEnvelope(Sequence(1), pid.id, 1, GenericWrapper(Incremented(1)), 0L))
val c2 =
spawn(Behaviors.setup[Command](ctx => counter(ctx, pid).eventAdapter(new GenericWrapperEventAdapter[Event])))
c2 ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(1, Vector(0)))
}
"adapter multiple events with persist all" in {
val pid = nextPid()
val c =
spawn(Behaviors.setup[Command](ctx => counter(ctx, pid).eventAdapter(new GenericWrapperEventAdapter[Event])))
val replyProbe = TestProbe[State]()
c ! IncrementWithPersistAll(2)
c ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(2, Vector(0, 1)))
val events = queries.currentEventsByPersistenceId(pid.id).runWith(Sink.seq).futureValue
events shouldEqual List(
EventEnvelope(Sequence(1), pid.id, 1, GenericWrapper(Incremented(1)), 0L),
EventEnvelope(Sequence(2), pid.id, 2, GenericWrapper(Incremented(1)), 0L))
val c2 =
spawn(Behaviors.setup[Command](ctx => counter(ctx, pid).eventAdapter(new GenericWrapperEventAdapter[Event])))
c2 ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(2, Vector(0, 1)))
}
"adapt and tag events" in {
val pid = nextPid()
val c = spawn(Behaviors.setup[Command](ctx =>
counter(ctx, pid).withTagger(_ => Set("tag99")).eventAdapter(new GenericWrapperEventAdapter[Event])))
val replyProbe = TestProbe[State]()
c ! Increment
c ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(1, Vector(0)))
val events = queries.currentEventsByPersistenceId(pid.id).runWith(Sink.seq).futureValue
events shouldEqual List(EventEnvelope(Sequence(1), pid.id, 1, GenericWrapper(Incremented(1)), 0L))
val c2 =
spawn(Behaviors.setup[Command](ctx => counter(ctx, pid).eventAdapter(new GenericWrapperEventAdapter[Event])))
c2 ! GetValue(replyProbe.ref)
replyProbe.expectMessage(State(1, Vector(0)))
val taggedEvents = queries.currentEventsByTag("tag99").runWith(Sink.seq).futureValue
taggedEvents shouldEqual List(EventEnvelope(Sequence(1), pid.id, 1, GenericWrapper(Incremented(1)), 0L))
}
}
}

View file

@ -0,0 +1,165 @@
/*
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.testkit.typed.scaladsl.TestProbe
import pekko.actor.typed.scaladsl.Behaviors
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.RecoveryCompleted
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
object EventSourcedSequenceNumberSpec {
private val conf = ConfigFactory.parseString(s"""
pekko.persistence.journal.plugin = "pekko.persistence.journal.inmem"
pekko.persistence.journal.inmem.test-serialization = on
pekko.persistence.snapshot-store.plugin = "slow-snapshot-store"
slow-snapshot-store.class = "${classOf[SlowInMemorySnapshotStore].getName}"
""")
}
class EventSourcedSequenceNumberSpec
extends ScalaTestWithActorTestKit(EventSourcedSequenceNumberSpec.conf)
with AnyWordSpecLike
with LogCapturing {
private def behavior(pid: PersistenceId, probe: ActorRef[String]): Behavior[String] =
Behaviors.setup(ctx =>
EventSourcedBehavior[String, String, String](pid, "",
{
(state, command) =>
state match {
case "stashing" =>
command match {
case "unstash" =>
probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} unstash"
Effect.persist("normal").thenUnstashAll()
case _ =>
Effect.stash()
}
case _ =>
command match {
case "cmd" =>
probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} onCommand"
Effect
.persist("evt")
.thenRun(_ => probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} thenRun")
case "cmd3" =>
probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} onCommand"
Effect
.persist("evt1", "evt2", "evt3")
.thenRun(_ => probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} thenRun")
case "stash" =>
probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} stash"
Effect.persist("stashing")
case "snapshot" =>
Effect.persist("snapshot")
}
}
},
{ (_, evt) =>
probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} eventHandler $evt"
evt
}).snapshotWhen((_, event, _) => event == "snapshot").receiveSignal {
case (_, RecoveryCompleted) =>
probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} onRecoveryComplete"
})
"The sequence number" must {
"be accessible in the handlers" in {
val probe = TestProbe[String]()
val ref = spawn(behavior(PersistenceId.ofUniqueId("ess-1"), probe.ref))
probe.expectMessage("0 onRecoveryComplete")
ref ! "cmd"
probe.expectMessage("0 onCommand")
probe.expectMessage("1 eventHandler evt")
probe.expectMessage("1 thenRun")
ref ! "cmd"
probe.expectMessage("1 onCommand")
probe.expectMessage("2 eventHandler evt")
probe.expectMessage("2 thenRun")
ref ! "cmd3"
probe.expectMessage("2 onCommand")
probe.expectMessage("3 eventHandler evt1")
probe.expectMessage("4 eventHandler evt2")
probe.expectMessage("5 eventHandler evt3")
probe.expectMessage("5 thenRun")
testKit.stop(ref)
probe.expectTerminated(ref)
// and during replay
val ref2 = spawn(behavior(PersistenceId.ofUniqueId("ess-1"), probe.ref))
probe.expectMessage("1 eventHandler evt")
probe.expectMessage("2 eventHandler evt")
probe.expectMessage("3 eventHandler evt1")
probe.expectMessage("4 eventHandler evt2")
probe.expectMessage("5 eventHandler evt3")
probe.expectMessage("5 onRecoveryComplete")
ref2 ! "cmd"
probe.expectMessage("5 onCommand")
probe.expectMessage("6 eventHandler evt")
probe.expectMessage("6 thenRun")
}
"be available while unstashing" in {
val probe = TestProbe[String]()
val ref = spawn(behavior(PersistenceId.ofUniqueId("ess-2"), probe.ref))
probe.expectMessage("0 onRecoveryComplete")
ref ! "stash"
ref ! "cmd"
ref ! "cmd"
ref ! "cmd3"
ref ! "unstash"
probe.expectMessage("0 stash")
probe.expectMessage("1 eventHandler stashing")
probe.expectMessage("1 unstash")
probe.expectMessage("2 eventHandler normal")
probe.expectMessage("2 onCommand")
probe.expectMessage("3 eventHandler evt")
probe.expectMessage("3 thenRun")
probe.expectMessage("3 onCommand")
probe.expectMessage("4 eventHandler evt")
probe.expectMessage("4 thenRun")
probe.expectMessage("4 onCommand") // cmd3
probe.expectMessage("5 eventHandler evt1")
probe.expectMessage("6 eventHandler evt2")
probe.expectMessage("7 eventHandler evt3")
probe.expectMessage("7 thenRun")
}
// reproducer for #27935
"not fail when snapshotting" in {
val probe = TestProbe[String]()
val ref = spawn(behavior(PersistenceId.ofUniqueId("ess-3"), probe.ref))
probe.expectMessage("0 onRecoveryComplete")
ref ! "cmd"
ref ! "snapshot"
ref ! "cmd"
probe.expectMessage("0 onCommand") // first command
probe.expectMessage("1 eventHandler evt")
probe.expectMessage("1 thenRun")
probe.expectMessage("2 eventHandler snapshot")
probe.expectMessage("2 onCommand") // second command
probe.expectMessage("3 eventHandler evt")
probe.expectMessage("3 thenRun")
}
}
}

View file

@ -0,0 +1,105 @@
/*
* Copyright (C) 2019-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.testkit.typed.scaladsl.TestProbe
import pekko.actor.typed.ActorRef
import pekko.persistence.query.PersistenceQuery
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.testkit.PersistenceTestKitSnapshotPlugin
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.SnapshotAdapter
import pekko.serialization.jackson.CborSerializable
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.concurrent.atomic.AtomicInteger
object EventSourcedSnapshotAdapterSpec {
case class State(s: String) extends CborSerializable
case class Command(c: String) extends CborSerializable
case class Event(e: String) extends CborSerializable
case class PersistedState(s: String) extends CborSerializable
}
class EventSourcedSnapshotAdapterSpec
extends ScalaTestWithActorTestKit(
PersistenceTestKitPlugin.config.withFallback(PersistenceTestKitSnapshotPlugin.config))
with AnyWordSpecLike
with LogCapturing {
import EventSourcedSnapshotAdapterSpec._
import pekko.actor.typed.scaladsl.adapter._
val pidCounter = new AtomicInteger(0)
private def nextPid(): PersistenceId = PersistenceId.ofUniqueId(s"c${pidCounter.incrementAndGet()})")
val queries: PersistenceTestKitReadJournal =
PersistenceQuery(system.toClassic)
.readJournalFor[PersistenceTestKitReadJournal](PersistenceTestKitReadJournal.Identifier)
private def behavior(pid: PersistenceId, probe: ActorRef[State]): EventSourcedBehavior[Command, Event, State] =
EventSourcedBehavior[Command, Event, State](
pid,
State(""),
commandHandler = { (state, command) =>
command match {
case Command(c) if c == "shutdown" =>
Effect.stop()
case Command(c) if c == "get" =>
probe.tell(state)
Effect.none
case _ =>
Effect.persist(Event(command.c)).thenRun(newState => probe ! newState)
}
},
eventHandler = { (state, evt) =>
state.copy(s = state.s + "|" + evt.e)
})
"Snapshot adapter" must {
"adapt snapshots to any" in {
val pid = nextPid()
val stateProbe = TestProbe[State]()
val snapshotFromJournal = TestProbe[PersistedState]()
val snapshotToJournal = TestProbe[State]()
val b = behavior(pid, stateProbe.ref)
.snapshotAdapter(new SnapshotAdapter[State]() {
override def toJournal(state: State): Any = {
snapshotToJournal.ref.tell(state)
PersistedState(state.s)
}
override def fromJournal(from: Any): State = from match {
case ps: PersistedState =>
snapshotFromJournal.ref.tell(ps)
State(ps.s)
case unexpected => throw new RuntimeException(s"Unexpected: $unexpected")
}
})
.snapshotWhen { (_, event, _) =>
event.e.contains("snapshot")
}
val ref = spawn(b)
ref.tell(Command("one"))
stateProbe.expectMessage(State("|one"))
ref.tell(Command("snapshot now"))
stateProbe.expectMessage(State("|one|snapshot now"))
snapshotToJournal.expectMessage(State("|one|snapshot now"))
ref.tell(Command("shutdown"))
val ref2 = spawn(b)
snapshotFromJournal.expectMessage(PersistedState("|one|snapshot now"))
ref2.tell(Command("get"))
stateProbe.expectMessage(State("|one|snapshot now"))
}
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.Done
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.adapter._
import pekko.persistence.journal.SteppingInmemJournal
import pekko.persistence.typed.PersistenceId
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
import scala.concurrent.duration._
// Reproducer for #29401
object EventSourcedStashOverflowSpec {
object EventSourcedStringList {
sealed trait Command
case class DoNothing(replyTo: ActorRef[Done]) extends Command
def apply(persistenceId: PersistenceId): Behavior[Command] =
EventSourcedBehavior[Command, String, List[String]](
persistenceId,
Nil,
{ (_, command) =>
command match {
case DoNothing(replyTo) =>
Effect.persist(List.empty[String]).thenRun(_ => replyTo ! Done)
}
},
{ (state, event) =>
// original reproducer slept 2 seconds here but a pure application of an event seems unlikely to take that long
// so instead we delay recovery using a special journal
event :: state
})
}
def conf =
SteppingInmemJournal.config("EventSourcedStashOverflow").withFallback(ConfigFactory.parseString(s"""
pekko.persistence {
typed {
stash-capacity = 1000 # enough to fail on stack size
stash-overflow-strategy = "drop"
}
}
"""))
}
class EventSourcedStashOverflowSpec
extends ScalaTestWithActorTestKit(EventSourcedStashOverflowSpec.conf)
with AnyWordSpecLike
with LogCapturing {
import EventSourcedStashOverflowSpec.EventSourcedStringList
"Stashing in a busy event sourced behavior" must {
"not cause stack overflow" in {
val es = spawn(EventSourcedStringList(PersistenceId.ofUniqueId("id-1")))
// wait for journal to start
val probe = testKit.createTestProbe[Done]()
probe.awaitAssert(SteppingInmemJournal.getRef("EventSourcedStashOverflow"), 3.seconds)
val journal = SteppingInmemJournal.getRef("EventSourcedStashOverflow")
val droppedMessageProbe = testKit.createDroppedMessageProbe()
val stashCapacity = testKit.config.getInt("pekko.persistence.typed.stash-capacity")
for (_ <- 0 to (stashCapacity * 2)) {
es.tell(EventSourcedStringList.DoNothing(probe.ref))
}
// capacity + 1 should mean that we get a dropped last message when all stash is filled
// while the actor is stuck in replay because journal isn't responding
droppedMessageProbe.receiveMessage()
implicit val classicSystem: pekko.actor.ActorSystem =
testKit.system.toClassic
// we only need to do this one step and recovery completes
SteppingInmemJournal.step(journal)
// exactly how many is racy but at least the first stash buffer full should complete
probe.receiveMessages(stashCapacity)
}
}
}

View file

@ -0,0 +1,118 @@
/*
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.LoggingTestKit
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.RecoveryCompleted
import pekko.persistence.typed.SnapshotCompleted
import pekko.persistence.typed.SnapshotFailed
import org.scalatest.wordspec.AnyWordSpecLike
import org.slf4j.event.Level
import java.util.concurrent.atomic.AtomicInteger
// Note that the spec name here is important since there are heuristics in place to avoid names
// starting with EventSourcedBehavior
class LoggerSourceSpec
extends ScalaTestWithActorTestKit(EventSourcedBehaviorSpec.conf)
with AnyWordSpecLike
with LogCapturing {
private val pidCounter = new AtomicInteger(0)
private def nextPid(): PersistenceId = PersistenceId.ofUniqueId(s"c${pidCounter.incrementAndGet()})")
def behavior: Behavior[String] = Behaviors.setup { ctx =>
ctx.log.info("setting-up-behavior")
EventSourcedBehavior[String, String, String](nextPid(), emptyState = "",
commandHandler = (_, _) => {
ctx.log.info("command-received")
Effect.persist("evt")
},
eventHandler = (state, _) => {
ctx.log.info("event-received")
state
}).receiveSignal {
case (_, RecoveryCompleted) => ctx.log.info("recovery-completed")
case (_, SnapshotCompleted(_)) =>
case (_, SnapshotFailed(_, _)) =>
}
}
"log from context" should {
// note that these are somewhat intermingled to make sure no log event from
// one test case leaks to another, the actual log class is what is tested in each individual case
"log from setup" in {
LoggingTestKit.info("recovery-completed").expect {
eventFilterFor("setting-up-behavior").expect {
spawn(behavior)
}
}
}
"log from recovery completed" in {
LoggingTestKit.info("setting-up-behavior").expect {
eventFilterFor("recovery-completed").expect {
spawn(behavior)
}
}
}
"log from command handler" in {
LoggingTestKit.empty
.withLogLevel(Level.INFO)
.withMessageRegex("(setting-up-behavior|recovery-completed|event-received)")
.withOccurrences(3)
.expect {
eventFilterFor("command-received").expect {
spawn(behavior) ! "cmd"
}
}
}
"log from event handler" in {
LoggingTestKit.empty
.withLogLevel(Level.INFO)
.withMessageRegex("(setting-up-behavior|recovery-completed|command-received)")
.withOccurrences(3)
.expect {
eventFilterFor("event-received").expect {
spawn(behavior) ! "cmd"
}
}
}
"use the user provided name" in {
val behavior: Behavior[String] = Behaviors.setup[String] { ctx =>
ctx.setLoggerName("my-custom-name")
EventSourcedBehavior[String, String, String](nextPid(), emptyState = "",
commandHandler = (_, _) => {
ctx.log.info("command-received")
Effect.persist("evt")
}, eventHandler = (state, _) => state)
}
val actor = spawn(behavior)
LoggingTestKit.info("command-received").withLoggerName("my-custom-name").withOccurrences(1).expect {
actor ! "cmd"
}
}
}
def eventFilterFor(logMsg: String) =
LoggingTestKit.custom { logEvent =>
logEvent.message == logMsg && logEvent.loggerName == classOf[LoggerSourceSpec].getName
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.TestKitSettings
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.RecoveryCompleted
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
object NullEmptyStateSpec {
private val conf = ConfigFactory.parseString(s"""
pekko.persistence.journal.plugin = "pekko.persistence.journal.inmem"
pekko.persistence.journal.inmem.test-serialization = on
""")
}
class NullEmptyStateSpec
extends ScalaTestWithActorTestKit(NullEmptyStateSpec.conf)
with AnyWordSpecLike
with LogCapturing {
implicit val testSettings: TestKitSettings = TestKitSettings(system)
def nullState(persistenceId: PersistenceId, probe: ActorRef[String]): Behavior[String] =
EventSourcedBehavior[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
}).receiveSignal {
case (state, RecoveryCompleted) =>
probe.tell("onRecoveryCompleted:" + state)
}
"A typed persistent actor with null empty state" must {
"persist events and update state" in {
val probe = TestProbe[String]()
val b = nullState(PersistenceId.ofUniqueId("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"
// wait till ref1 stops
probe.expectTerminated(ref1)
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")
}
}
}

View file

@ -0,0 +1,83 @@
/*
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.LoggingTestKit
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.testkit.typed.scaladsl.TestProbe
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.scaladsl.EventSourcedBehavior.CommandHandler
import pekko.serialization.jackson.CborSerializable
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.UUID
object OptionalSnapshotStoreSpec {
sealed trait Command extends CborSerializable
object AnyCommand extends Command
final case class State(internal: String = UUID.randomUUID().toString) extends CborSerializable
case class Event(id: Long = System.currentTimeMillis()) extends CborSerializable
def persistentBehavior(probe: TestProbe[State], name: String = UUID.randomUUID().toString) =
EventSourcedBehavior[Command, Event, State](
persistenceId = PersistenceId.ofUniqueId(name),
emptyState = State(),
commandHandler = CommandHandler.command { _ =>
Effect.persist(Event()).thenRun(probe.ref ! _)
},
eventHandler = {
case (_, _) => State()
}).snapshotWhen { case _ => true }
def persistentBehaviorWithSnapshotPlugin(probe: TestProbe[State]) =
persistentBehavior(probe).withSnapshotPluginId("pekko.persistence.snapshot-store.local")
}
class OptionalSnapshotStoreSpec extends ScalaTestWithActorTestKit(s"""
pekko.persistence.publish-plugin-commands = on
pekko.persistence.journal.plugin = "pekko.persistence.journal.inmem"
pekko.persistence.journal.inmem.test-serialization = on
# snapshot store plugin is NOT defined, things should still work
pekko.persistence.snapshot-store.local.dir = "target/snapshots-${classOf[OptionalSnapshotStoreSpec].getName}/"
""") with AnyWordSpecLike with LogCapturing {
import OptionalSnapshotStoreSpec._
"Persistence extension" must {
"initialize properly even in absence of configured snapshot store" in {
LoggingTestKit.warn("No default snapshot store configured").expect {
val stateProbe = TestProbe[State]()
spawn(persistentBehavior(stateProbe))
stateProbe.expectNoMessage()
}
}
"fail if PersistentActor tries to saveSnapshot without snapshot-store available" in {
LoggingTestKit.error("No snapshot store configured").expect {
LoggingTestKit.warn("Failed to save snapshot").expect {
val stateProbe = TestProbe[State]()
val persistentActor = spawn(persistentBehavior(stateProbe))
persistentActor ! AnyCommand
stateProbe.expectMessageType[State]
}
}
}
"successfully save a snapshot when no default snapshot-store configured, yet PersistentActor picked one explicitly" in {
val stateProbe = TestProbe[State]()
val persistentActor = spawn(persistentBehaviorWithSnapshotPlugin(stateProbe))
persistentActor ! AnyCommand
stateProbe.expectMessageType[State]
}
}
}

View file

@ -0,0 +1,167 @@
/*
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.TestException
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.testkit.typed.scaladsl.TestProbe
import pekko.actor.typed.ActorRef
import pekko.actor.typed.SupervisorStrategy
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.testkit.PersistenceTestKitSnapshotPlugin
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.RecoveryCompleted
import pekko.persistence.typed.scaladsl.EventSourcedBehavior.CommandHandler
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.UUID
import scala.concurrent.duration._
object PerformanceSpec {
val config =
"""
pekko.persistence.performance.cycles.load = 100
# more accurate throughput measurements
#pekko.persistence.performance.cycles.load = 10000
# no stash capacity limit
pekko.persistence.typed.stash-capacity = 1000000
"""
sealed trait Command
case object StopMeasure extends Command with Reply
case class FailAt(sequence: Long) extends Command
case class CommandWithEvent(evt: String) extends Command
sealed trait Reply
case object ExpectedFail extends Reply
class Measure(numberOfMessages: Int) {
private val NanoToSecond = 1000.0 * 1000 * 1000
private var startTime: Long = 0L
private var stopTime: Long = 0L
def startMeasure(): Unit = {
startTime = System.nanoTime
}
def stopMeasure(): Double = {
stopTime = System.nanoTime
NanoToSecond * numberOfMessages / (stopTime - startTime)
}
}
case class Parameters(var persistCalls: Long = 0L, var failAt: Long = -1) {
def every(num: Long): Boolean = persistCalls % num == 0
def shouldFail: Boolean =
failAt != -1 && persistCalls % failAt == 0
def failureWasDefined: Boolean = failAt != -1L
}
def behavior(name: String, probe: TestProbe[Reply])(other: (Command, Parameters) => Effect[String, String]) = {
Behaviors
.supervise {
val parameters = Parameters()
EventSourcedBehavior[Command, String, String](
persistenceId = PersistenceId.ofUniqueId(name),
"",
commandHandler = CommandHandler.command {
case StopMeasure =>
Effect.none.thenRun(_ => probe.ref ! StopMeasure)
case FailAt(sequence) =>
Effect.none.thenRun(_ => parameters.failAt = sequence)
case command => other(command, parameters)
},
eventHandler = {
case (state, _) => state
}).receiveSignal {
case (_, RecoveryCompleted) =>
if (parameters.every(1000)) print("r")
}
}
.onFailure(SupervisorStrategy.restart.withLoggingEnabled(false))
}
def eventSourcedTestPersistenceBehavior(name: String, probe: TestProbe[Reply]) =
behavior(name, probe) {
case (CommandWithEvent(evt), parameters) =>
Effect
.persist(evt)
.thenRun(_ => {
parameters.persistCalls += 1
if (parameters.every(1000)) print(".")
if (parameters.shouldFail) {
probe.ref ! ExpectedFail
throw TestException("boom")
}
})
case _ => Effect.none
}
}
class PerformanceSpec
extends ScalaTestWithActorTestKit(
PersistenceTestKitPlugin.config
.withFallback(PersistenceTestKitSnapshotPlugin.config)
.withFallback(ConfigFactory.parseString(s"""
pekko.persistence.publish-plugin-commands = on
pekko.actor.testkit.typed.single-expect-default = 10s
"""))
.withFallback(ConfigFactory.parseString(PerformanceSpec.config)))
with AnyWordSpecLike
with LogCapturing {
import PerformanceSpec._
val loadCycles = system.settings.config.getInt("pekko.persistence.performance.cycles.load")
def stressPersistentActor(
persistentActor: ActorRef[Command],
probe: TestProbe[Reply],
failAt: Option[Long],
description: String): Unit = {
failAt.foreach { persistentActor ! FailAt(_) }
val m = new Measure(loadCycles)
m.startMeasure()
val parameters = Parameters(0, failAt = failAt.getOrElse(-1))
(1 to loadCycles).foreach { n =>
parameters.persistCalls += 1
persistentActor ! CommandWithEvent(s"msg$n")
// stash is cleared when exception is thrown so have to wait before sending more commands
if (parameters.shouldFail)
probe.expectMessage(ExpectedFail)
}
persistentActor ! StopMeasure
probe.expectMessage(100.seconds, StopMeasure)
println(f"\nthroughput = ${m.stopMeasure()}%.2f $description per second")
}
def stressEventSourcedPersistentActor(failAt: Option[Long]): Unit = {
val probe = TestProbe[Reply]()
val name = s"${this.getClass.getSimpleName}-${UUID.randomUUID().toString}"
val persistentActor = spawn(eventSourcedTestPersistenceBehavior(name, probe), name)
stressPersistentActor(persistentActor, probe, failAt, "persistent events")
}
"An event sourced persistent actor" should {
"have some reasonable throughput" in {
stressEventSourcedPersistentActor(None)
}
"have some reasonable throughput under failure conditions" in {
stressEventSourcedPersistentActor(Some((loadCycles / 10).toLong))
}
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.RecoveryCompleted
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
object PrimitiveStateSpec {
private val conf = ConfigFactory.parseString(s"""
pekko.persistence.journal.plugin = "pekko.persistence.journal.inmem"
pekko.persistence.journal.inmem.test-serialization = on
""")
}
class PrimitiveStateSpec
extends ScalaTestWithActorTestKit(PrimitiveStateSpec.conf)
with AnyWordSpecLike
with LogCapturing {
def primitiveState(persistenceId: PersistenceId, probe: ActorRef[String]): Behavior[Int] =
EventSourcedBehavior[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
}).receiveSignal {
case (n, RecoveryCompleted) =>
probe.tell("onRecoveryCompleted:" + n)
}
"A typed persistent actor with primitive state" must {
"persist primitive events and update state" in {
val probe = TestProbe[String]()
val b = primitiveState(PersistenceId.ofUniqueId("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
probe.expectTerminated(ref1)
val ref2 = spawn(b)
// eventHandler from replay
probe.expectMessage("eventHandler:0:1")
probe.expectMessage("eventHandler:1:2")
probe.expectMessage("onRecoveryCompleted:3")
ref2 ! 3
probe.expectMessage("eventHandler:3:3")
}
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.persistence.SelectedSnapshot
import pekko.persistence.snapshot.SnapshotStore
import pekko.persistence.typed.scaladsl.SnapshotMutableStateSpec.MutableState
import pekko.persistence.{ SnapshotMetadata => ClassicSnapshotMetadata }
import pekko.persistence.{ SnapshotSelectionCriteria => ClassicSnapshotSelectionCriteria }
import scala.concurrent.Future
class SlowInMemorySnapshotStore extends SnapshotStore {
private var state = Map.empty[String, (Any, ClassicSnapshotMetadata)]
def loadAsync(persistenceId: String, criteria: ClassicSnapshotSelectionCriteria): Future[Option[SelectedSnapshot]] = {
Future.successful(state.get(persistenceId).map {
case (snap, meta) => SelectedSnapshot(meta, snap)
})
}
def saveAsync(metadata: ClassicSnapshotMetadata, snapshot: Any): Future[Unit] = {
val snapshotState = snapshot.asInstanceOf[MutableState]
val value1 = snapshotState.value
Thread.sleep(50)
val value2 = snapshotState.value
// it mustn't have been modified by another command/event
if (value1 != value2)
Future.failed(new IllegalStateException(s"State changed from $value1 to $value2"))
else {
// copy to simulate serialization, and subsequent recovery shouldn't get same instance
state = state.updated(metadata.persistenceId, (new MutableState(snapshotState.value), metadata))
Future.successful(())
}
}
override def deleteAsync(metadata: ClassicSnapshotMetadata): Future[Unit] = {
state = state.filterNot {
case (pid, (_, meta)) => pid == metadata.persistenceId && meta.sequenceNr == metadata.sequenceNr
}
Future.successful(())
}
override def deleteAsync(persistenceId: String, criteria: ClassicSnapshotSelectionCriteria): Future[Unit] = {
state = state.filterNot {
case (pid, (_, meta)) => pid == persistenceId && criteria.matches(meta)
}
Future.successful(())
}
}

View file

@ -0,0 +1,114 @@
/*
* Copyright (C) 2017-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.SnapshotCompleted
import pekko.persistence.typed.SnapshotFailed
import pekko.serialization.jackson.CborSerializable
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.concurrent.atomic.AtomicInteger
object SnapshotMutableStateSpec {
def conf: Config = PersistenceTestKitPlugin.config.withFallback(ConfigFactory.parseString(s"""
pekko.loglevel = INFO
pekko.persistence.snapshot-store.plugin = "slow-snapshot-store"
slow-snapshot-store.class = "${classOf[SlowInMemorySnapshotStore].getName}"
"""))
sealed trait Command extends CborSerializable
case object Increment extends Command
final case class GetValue(replyTo: ActorRef[Int]) extends Command
sealed trait Event extends CborSerializable
case object Incremented extends Event
final class MutableState(var value: Int) extends CborSerializable
def counter(
persistenceId: PersistenceId,
probe: ActorRef[String]): EventSourcedBehavior[Command, Event, MutableState] = {
EventSourcedBehavior[Command, Event, MutableState](
persistenceId,
emptyState = new MutableState(0),
commandHandler = (state, cmd) =>
cmd match {
case Increment =>
Effect.persist(Incremented)
case GetValue(replyTo) =>
replyTo ! state.value
Effect.none
},
eventHandler = (state, evt) =>
evt match {
case Incremented =>
state.value += 1
probe ! s"incremented-${state.value}"
state
}).receiveSignal {
case (_, SnapshotCompleted(meta)) =>
probe ! s"snapshot-success-${meta.sequenceNr}"
case (_, SnapshotFailed(meta, _)) =>
probe ! s"snapshot-failure-${meta.sequenceNr}"
}
}
}
class SnapshotMutableStateSpec
extends ScalaTestWithActorTestKit(SnapshotMutableStateSpec.conf)
with AnyWordSpecLike
with LogCapturing {
import SnapshotMutableStateSpec._
val pidCounter = new AtomicInteger(0)
private def nextPid(): PersistenceId = PersistenceId.ofUniqueId(s"c${pidCounter.incrementAndGet()})")
"A typed persistent actor with mutable state" must {
"support mutable state by stashing commands while storing snapshot" in {
val pid = nextPid()
val probe = TestProbe[String]()
def snapshotState3: Behavior[Command] =
counter(pid, probe.ref).snapshotWhen { (state, _, _) =>
state.value == 3
}
val c = spawn(snapshotState3)
(1 to 5).foreach { n =>
c ! Increment
probe.expectMessage(s"incremented-$n")
if (n == 3) {
// incremented-4 shouldn't be before the snapshot-success-3, because Increment 4 is stashed
probe.expectMessage(s"snapshot-success-3")
}
}
val replyProbe = TestProbe[Int]()
c ! GetValue(replyProbe.ref)
replyProbe.expectMessage(5)
// recover new instance
val c2 = spawn(snapshotState3)
// starting from snapshot 3
probe.expectMessage(s"incremented-4")
probe.expectMessage(s"incremented-5")
c2 ! GetValue(replyProbe.ref)
replyProbe.expectMessage(5)
}
}
}

View file

@ -0,0 +1,96 @@
/*
* Copyright (C) 2019-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.serialization.Snapshot
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.typed.PersistenceId
import pekko.serialization.Serialization
import pekko.serialization.SerializationExtension
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import org.apache.commons.io.FileUtils
import org.scalatest.wordspec.AnyWordSpecLike
import java.io.File
import java.util.UUID
object SnapshotRecoveryWithEmptyJournalSpec {
val survivingSnapshotPath = s"target/survivingSnapshotPath-${UUID.randomUUID().toString}"
def conf: Config = PersistenceTestKitPlugin.config.withFallback(ConfigFactory.parseString(s"""
pekko.loglevel = INFO
pekko.persistence.snapshot-store.plugin = "pekko.persistence.snapshot-store.local"
pekko.persistence.snapshot-store.local.dir = "${SnapshotRecoveryWithEmptyJournalSpec.survivingSnapshotPath}"
pekko.actor.allow-java-serialization = on
pekko.actor.warn-about-java-serializer-usage = off
"""))
object TestActor {
def apply(name: String, probe: ActorRef[Any]): Behavior[String] = {
Behaviors.setup { context =>
EventSourcedBehavior[String, String, List[String]](
PersistenceId.ofUniqueId(name),
Nil,
(state, cmd) =>
cmd match {
case "get" =>
probe ! state.reverse
Effect.none
case _ =>
Effect.persist(s"$cmd-${EventSourcedBehavior.lastSequenceNumber(context) + 1}")
},
(state, event) => event :: state)
}
}
}
}
class SnapshotRecoveryWithEmptyJournalSpec
extends ScalaTestWithActorTestKit(SnapshotRecoveryWithEmptyJournalSpec.conf)
with AnyWordSpecLike
with LogCapturing {
import SnapshotRecoveryWithEmptyJournalSpec._
val snapshotsDir: File = new File(survivingSnapshotPath)
val serializationExtension: Serialization = SerializationExtension(system)
val persistenceId: String = system.name
// Prepare a hand made snapshot file as basis for the recovery start point
private def createSnapshotFile(sequenceNr: Long, ts: Long, data: Any): Unit = {
val snapshotFile = new File(snapshotsDir, s"snapshot-$persistenceId-$sequenceNr-$ts")
FileUtils.writeByteArrayToFile(snapshotFile, serializationExtension.serialize(Snapshot(data)).get)
}
val givenSnapshotSequenceNr: Long = 4711L
val givenTimestamp: Long = 1000L
override protected def beforeAll(): Unit = {
super.beforeAll()
createSnapshotFile(givenSnapshotSequenceNr - 1, givenTimestamp - 1, List("a-1"))
createSnapshotFile(givenSnapshotSequenceNr, givenTimestamp, List("b-2", "a-1"))
}
"A persistent actor in a system that only has snapshots and no previous journal activity" must {
"recover its state and sequence number starting from the most recent snapshot and use subsequent sequence numbers to persist events to the journal" in {
val probe = createTestProbe[Any]()
val ref = spawn(TestActor(persistenceId, probe.ref))
ref ! "c"
ref ! "get"
probe.expectMessage(List("a-1", "b-2", s"c-${givenSnapshotSequenceNr + 1}"))
}
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (C) 2019-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.state.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.BehaviorInterceptor
import pekko.actor.typed.TypedActorContext
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.typed.PersistenceId
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.concurrent.atomic.AtomicInteger
import pekko.persistence.testkit.PersistenceTestKitDurableStateStorePlugin
object DurableStateBehaviorInterceptorSpec {
def conf: Config = PersistenceTestKitDurableStateStorePlugin.config.withFallback(ConfigFactory.parseString(s"""
pekko.loglevel = INFO
"""))
def testBehavior(persistenceId: PersistenceId, probe: ActorRef[String]): Behavior[String] =
Behaviors.setup { _ =>
DurableStateBehavior[String, String](
persistenceId,
emptyState = "",
commandHandler = (_, command) =>
command match {
case _ =>
Effect.persist(command).thenRun(newState => probe ! newState)
})
}
}
class DurableStateBehaviorInterceptorSpec
extends ScalaTestWithActorTestKit(DurableStateBehaviorInterceptorSpec.conf)
with AnyWordSpecLike
with LogCapturing {
import DurableStateBehaviorInterceptorSpec._
val pidCounter = new AtomicInteger(0)
private def nextPid(): PersistenceId = PersistenceId.ofUniqueId(s"c${pidCounter.incrementAndGet()})")
"DurableStateBehavior interceptor" must {
"be possible to combine with another interceptor" in {
val probe = createTestProbe[String]()
val pid = nextPid()
val toUpper = new BehaviorInterceptor[String, String] {
override def aroundReceive(
ctx: TypedActorContext[String],
msg: String,
target: BehaviorInterceptor.ReceiveTarget[String]): Behavior[String] = {
target(ctx, msg.toUpperCase())
}
}
val ref = spawn(Behaviors.intercept(() => toUpper)(testBehavior(pid, probe.ref)))
ref ! "a"
ref ! "bc"
probe.expectMessage("A")
probe.expectMessage("BC")
}
"be possible to combine with transformMessages" in {
val probe = createTestProbe[String]()
val pid = nextPid()
val ref = spawn(testBehavior(pid, probe.ref).transformMessages[String] {
case s => s.toUpperCase()
})
ref ! "a"
ref ! "bc"
probe.expectMessage("A")
probe.expectMessage("BC")
}
"be possible to combine with MDC" in {
val probe = createTestProbe[String]()
val pid = nextPid()
val ref = spawn(Behaviors.setup[String] { _ =>
Behaviors.withMdc(
staticMdc = Map("pid" -> pid.toString),
mdcForMessage = (msg: String) => Map("msg" -> msg.toUpperCase())) {
testBehavior(pid, probe.ref)
}
})
ref ! "a"
ref ! "bc"
probe.expectMessage("a")
probe.expectMessage("bc")
}
}
}

View file

@ -0,0 +1,132 @@
/*
* Copyright (C) 2021-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.state.scaladsl
import java.util.concurrent.atomic.AtomicInteger
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
import org.apache.pekko
import pekko.Done
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.ActorContext
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.typed.PersistenceId
import pekko.serialization.jackson.CborSerializable
import pekko.persistence.testkit.PersistenceTestKitDurableStateStorePlugin
object DurableStateBehaviorReplySpec {
def conf: Config = PersistenceTestKitDurableStateStorePlugin.config.withFallback(ConfigFactory.parseString(s"""
pekko.loglevel = INFO
"""))
sealed trait Command[ReplyMessage] extends CborSerializable
final case class IncrementWithConfirmation(replyTo: ActorRef[Done]) extends Command[Done]
final case class IncrementReplyLater(replyTo: ActorRef[Done]) extends Command[Done]
final case class ReplyNow(replyTo: ActorRef[Done]) extends Command[Done]
final case class GetValue(replyTo: ActorRef[State]) extends Command[State]
final case class DeleteWithConfirmation(replyTo: ActorRef[Done]) extends Command[Done]
case object Increment extends Command[Nothing]
case class IncrementBy(by: Int) extends Command[Nothing]
final case class State(value: Int) extends CborSerializable
def counter(persistenceId: PersistenceId): Behavior[Command[_]] =
Behaviors.setup(ctx => counter(ctx, persistenceId))
def counter(ctx: ActorContext[Command[_]], persistenceId: PersistenceId): DurableStateBehavior[Command[_], State] = {
DurableStateBehavior.withEnforcedReplies[Command[_], State](
persistenceId,
emptyState = State(0),
commandHandler = (state, command) =>
command match {
case IncrementWithConfirmation(replyTo) =>
Effect.persist(state.copy(value = state.value + 1)).thenReply(replyTo)(_ => Done)
case IncrementReplyLater(replyTo) =>
Effect
.persist(state.copy(value = state.value + 1))
.thenRun((_: State) => ctx.self ! ReplyNow(replyTo))
.thenNoReply()
case ReplyNow(replyTo) =>
Effect.reply(replyTo)(Done)
case GetValue(replyTo) =>
Effect.reply(replyTo)(state)
case DeleteWithConfirmation(replyTo) =>
Effect.delete[State]().thenReply(replyTo)(_ => Done)
case _ => ???
})
}
}
class DurableStateBehaviorReplySpec
extends ScalaTestWithActorTestKit(DurableStateBehaviorReplySpec.conf)
with AnyWordSpecLike
with LogCapturing {
import DurableStateBehaviorReplySpec._
val pidCounter = new AtomicInteger(0)
private def nextPid(): PersistenceId = PersistenceId.ofUniqueId(s"c${pidCounter.incrementAndGet()})")
"A typed persistent actor with commands that are expecting replies" must {
"persist state thenReply" in {
val c = spawn(counter(nextPid()))
val probe = TestProbe[Done]()
c ! IncrementWithConfirmation(probe.ref)
probe.expectMessage(Done)
c ! IncrementWithConfirmation(probe.ref)
c ! IncrementWithConfirmation(probe.ref)
probe.expectMessage(Done)
probe.expectMessage(Done)
}
"persist state thenReply later" in {
val c = spawn(counter(nextPid()))
val probe = TestProbe[Done]()
c ! IncrementReplyLater(probe.ref)
probe.expectMessage(Done)
}
"reply to query command" in {
val c = spawn(counter(nextPid()))
val updateProbe = TestProbe[Done]()
c ! IncrementWithConfirmation(updateProbe.ref)
val queryProbe = TestProbe[State]()
c ! GetValue(queryProbe.ref)
queryProbe.expectMessage(State(1))
}
"delete state thenReply" in {
val c = spawn(counter(nextPid()))
val updateProbe = TestProbe[Done]()
c ! IncrementWithConfirmation(updateProbe.ref)
updateProbe.expectMessage(Done)
val deleteProbe = TestProbe[Done]()
c ! DeleteWithConfirmation(deleteProbe.ref)
deleteProbe.expectMessage(Done)
val queryProbe = TestProbe[State]()
c ! GetValue(queryProbe.ref)
queryProbe.expectMessage(State(0))
}
}
}

View file

@ -0,0 +1,119 @@
/*
* Copyright (C) 2021-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.state.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.typed.PersistenceId
import org.scalatest.wordspec.AnyWordSpecLike
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import java.util.concurrent.atomic.AtomicInteger
import scala.concurrent.duration._
import pekko.persistence.testkit.PersistenceTestKitDurableStateStorePlugin
object DurableStateBehaviorTimersSpec {
def conf: Config = PersistenceTestKitDurableStateStorePlugin.config.withFallback(ConfigFactory.parseString(s"""
pekko.loglevel = INFO
"""))
def testBehavior(persistenceId: PersistenceId, probe: ActorRef[String]): Behavior[String] =
Behaviors.setup { _ =>
Behaviors.withTimers { timers =>
DurableStateBehavior[String, String](
persistenceId,
emptyState = "",
commandHandler = (_, command) =>
command match {
case "scheduled" =>
probe ! "scheduled"
Effect.none
case "cmd-0" =>
timers.startSingleTimer("key", "scheduled", Duration.Zero)
Effect.none
case _ =>
timers.startSingleTimer("key", "scheduled", Duration.Zero)
Effect.persist(command).thenRun(_ => probe ! command)
})
}
}
def testTimerFromSetupBehavior(persistenceId: PersistenceId, probe: ActorRef[String]): Behavior[String] =
Behaviors.setup { _ =>
Behaviors.withTimers { timers =>
timers.startSingleTimer("key", "scheduled", Duration.Zero)
DurableStateBehavior[String, String](
persistenceId,
emptyState = "",
commandHandler = (_, command) =>
command match {
case "scheduled" =>
probe ! "scheduled"
Effect.none
case _ =>
Effect.persist(command).thenRun(_ => probe ! command)
})
}
}
}
class DurableStateBehaviorTimersSpec
extends ScalaTestWithActorTestKit(DurableStateBehaviorTimersSpec.conf)
with AnyWordSpecLike
with LogCapturing {
import DurableStateBehaviorTimersSpec._
val pidCounter = new AtomicInteger(0)
private def nextPid(): PersistenceId = PersistenceId.ofUniqueId(s"c${pidCounter.incrementAndGet()})")
"DurableStateBehavior withTimers" must {
"be able to schedule message" in {
val probe = createTestProbe[String]()
val pid = nextPid()
val ref = spawn(testBehavior(pid, probe.ref))
ref ! "cmd-0"
probe.expectMessage("scheduled")
}
"not discard timer msg due to stashing" in {
val probe = createTestProbe[String]()
val pid = nextPid()
val ref = spawn(testBehavior(pid, probe.ref))
ref ! "cmd-1"
probe.expectMessage("cmd-1")
probe.expectMessage("scheduled")
}
"be able to schedule message from setup" in {
val probe = createTestProbe[String]()
val pid = nextPid()
val ref = spawn(testTimerFromSetupBehavior(pid, probe.ref))
probe.expectMessage("scheduled")
(1 to 20).foreach { n =>
ref ! s"cmd-$n"
}
probe.receiveMessages(20)
// start new instance that is likely to stash the timer message while replaying
spawn(testTimerFromSetupBehavior(pid, probe.ref))
probe.expectMessage("scheduled")
}
}
}

View file

@ -0,0 +1,130 @@
/*
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.state.scaladsl
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.testkit.typed.scaladsl.TestProbe
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.Behaviors
import pekko.persistence.testkit.PersistenceTestKitDurableStateStorePlugin
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.state.RecoveryCompleted
object DurableStateRevisionSpec {
def conf: Config = PersistenceTestKitDurableStateStorePlugin.config.withFallback(ConfigFactory.parseString(s"""
pekko.loglevel = INFO
"""))
}
class DurableStateRevisionSpec
extends ScalaTestWithActorTestKit(DurableStateRevisionSpec.conf)
with AnyWordSpecLike
with LogCapturing {
private def behavior(pid: PersistenceId, probe: ActorRef[String]): Behavior[String] =
Behaviors.setup(ctx =>
DurableStateBehavior[String, String](
pid,
"",
(state, command) =>
state match {
case "stashing" =>
command match {
case "unstash" =>
probe ! s"${DurableStateBehavior.lastSequenceNumber(ctx)} unstash"
Effect.persist("normal").thenUnstashAll()
case _ =>
Effect.stash()
}
case _ =>
command match {
case "cmd" =>
probe ! s"${DurableStateBehavior.lastSequenceNumber(ctx)} onCommand"
Effect
.persist("state")
.thenRun(_ => probe ! s"${DurableStateBehavior.lastSequenceNumber(ctx)} thenRun")
case "stash" =>
probe ! s"${DurableStateBehavior.lastSequenceNumber(ctx)} stash"
Effect.persist("stashing")
case "snapshot" =>
Effect.persist("snapshot")
}
}).receiveSignal {
case (_, RecoveryCompleted) =>
probe ! s"${DurableStateBehavior.lastSequenceNumber(ctx)} onRecoveryComplete"
})
"The revision number" must {
"be accessible in the handlers" in {
val probe = TestProbe[String]()
val ref = spawn(behavior(PersistenceId.ofUniqueId("pid-1"), probe.ref))
probe.expectMessage("0 onRecoveryComplete")
ref ! "cmd"
probe.expectMessage("0 onCommand")
probe.expectMessage("1 thenRun")
ref ! "cmd"
probe.expectMessage("1 onCommand")
probe.expectMessage("2 thenRun")
testKit.stop(ref)
probe.expectTerminated(ref)
// and during recovery
val ref2 = spawn(behavior(PersistenceId.ofUniqueId("pid-1"), probe.ref))
probe.expectMessage("2 onRecoveryComplete")
ref2 ! "cmd"
probe.expectMessage("2 onCommand")
probe.expectMessage("3 thenRun")
}
"be available while unstashing" in {
val probe = TestProbe[String]()
val ref = spawn(behavior(PersistenceId.ofUniqueId("pid-2"), probe.ref))
probe.expectMessage("0 onRecoveryComplete")
ref ! "stash"
ref ! "cmd"
ref ! "cmd"
ref ! "cmd"
ref ! "unstash"
probe.expectMessage("0 stash")
probe.expectMessage("1 unstash")
probe.expectMessage("2 onCommand")
probe.expectMessage("3 thenRun")
probe.expectMessage("3 onCommand")
probe.expectMessage("4 thenRun")
probe.expectMessage("4 onCommand")
probe.expectMessage("5 thenRun")
}
"not fail when snapshotting" in {
val probe = TestProbe[String]()
val ref = spawn(behavior(PersistenceId.ofUniqueId("pid-3"), probe.ref))
probe.expectMessage("0 onRecoveryComplete")
ref ! "cmd"
ref ! "snapshot"
ref ! "cmd"
probe.expectMessage("0 onCommand") // first command
probe.expectMessage("1 thenRun")
probe.expectMessage("2 onCommand") // second command
probe.expectMessage("3 thenRun")
}
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (C) 2021-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.state.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.TestKitSettings
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.persistence.typed.PersistenceId
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
import pekko.persistence.testkit.PersistenceTestKitDurableStateStorePlugin
object NullEmptyStateSpec {
def conf: Config = PersistenceTestKitDurableStateStorePlugin.config.withFallback(ConfigFactory.parseString(s"""
pekko.loglevel = INFO
"""))
}
class NullEmptyStateSpec
extends ScalaTestWithActorTestKit(NullEmptyStateSpec.conf)
with AnyWordSpecLike
with LogCapturing {
implicit val testSettings: TestKitSettings = TestKitSettings(system)
def nullState(persistenceId: PersistenceId, probe: ActorRef[String]): Behavior[String] =
DurableStateBehavior[String, String](
persistenceId,
emptyState = null,
commandHandler = (state, command) => {
if (command == "stop")
Effect.stop()
else if (state == null)
Effect.persist(command).thenReply(probe)(newState => newState)
else
Effect.persist(s"$state:$command").thenReply(probe)(newState => newState)
})
"A typed persistent actor with null empty state" must {
"persist and update state" in {
val probe = TestProbe[String]()
val b = nullState(PersistenceId.ofUniqueId("a"), probe.ref)
val ref1 = spawn(b)
ref1 ! "one"
probe.expectMessage("one")
ref1 ! "two"
probe.expectMessage("one:two")
ref1 ! "stop"
probe.expectTerminated(ref1)
val ref2 = spawn(b)
ref2 ! "three"
probe.expectMessage("one:two:three")
}
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright (C) 2021-2022 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.persistence.typed.state.scaladsl
import org.apache.pekko
import pekko.actor.testkit.typed.scaladsl._
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.persistence.typed.PersistenceId
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpecLike
import pekko.persistence.testkit.PersistenceTestKitDurableStateStorePlugin
object PrimitiveStateSpec {
def conf: Config = PersistenceTestKitDurableStateStorePlugin.config.withFallback(ConfigFactory.parseString(s"""
pekko.loglevel = INFO
"""))
}
class PrimitiveStateSpec
extends ScalaTestWithActorTestKit(PrimitiveStateSpec.conf)
with AnyWordSpecLike
with LogCapturing {
def primitiveState(persistenceId: PersistenceId, probe: ActorRef[String]): Behavior[Int] =
DurableStateBehavior[Int, Int](
persistenceId,
emptyState = 0,
commandHandler = (state, command) => {
if (command < 0)
Effect.stop()
else
Effect.persist(state + command).thenReply(probe)(newState => newState.toString)
})
"A typed persistent actor with primitive state" must {
"persist primitive state and update" in {
val probe = TestProbe[String]()
val b = primitiveState(PersistenceId.ofUniqueId("a"), probe.ref)
val ref1 = spawn(b)
ref1 ! 1
probe.expectMessage("1")
ref1 ! 2
probe.expectMessage("3")
ref1 ! -1
probe.expectTerminated(ref1)
val ref2 = spawn(b)
ref2 ! 3
probe.expectMessage("6")
}
}
}