rename akka-docs dir to docs (#62)
This commit is contained in:
parent
13dce0ec69
commit
708da8caec
1029 changed files with 2033 additions and 2039 deletions
10
docs/src/test/java/jdocs/AbstractJavaTest.java
Normal file
10
docs/src/test/java/jdocs/AbstractJavaTest.java
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright (C) 2016-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs;
|
||||
|
||||
import org.scalatestplus.junit.JUnitSuite;
|
||||
|
||||
/** Base class for all runnable example tests written in Java */
|
||||
public abstract class AbstractJavaTest extends JUnitSuite {}
|
||||
899
docs/src/test/java/jdocs/actor/ActorDocTest.java
Normal file
899
docs/src/test/java/jdocs/actor/ActorDocTest.java
Normal file
|
|
@ -0,0 +1,899 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
import org.apache.pekko.actor.*;
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import com.typesafe.config.Config;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
import jdocs.AbstractJavaTest;
|
||||
import static jdocs.actor.Messages.Swap.Swap;
|
||||
import static jdocs.actor.Messages.*;
|
||||
import org.apache.pekko.actor.CoordinatedShutdown;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.time.Duration;
|
||||
import org.apache.pekko.testkit.TestActors;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
// #import-props
|
||||
import org.apache.pekko.actor.Props;
|
||||
// #import-props
|
||||
// #import-actorRef
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
// #import-actorRef
|
||||
// #import-identify
|
||||
import org.apache.pekko.actor.ActorIdentity;
|
||||
import org.apache.pekko.actor.ActorSelection;
|
||||
import org.apache.pekko.actor.Identify;
|
||||
// #import-identify
|
||||
// #import-ask
|
||||
import static org.apache.pekko.pattern.Patterns.ask;
|
||||
import static org.apache.pekko.pattern.Patterns.pipe;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
// #import-ask
|
||||
// #import-gracefulStop
|
||||
import static org.apache.pekko.pattern.Patterns.gracefulStop;
|
||||
import org.apache.pekko.pattern.AskTimeoutException;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
// #import-gracefulStop
|
||||
// #import-terminated
|
||||
import org.apache.pekko.actor.Terminated;
|
||||
// #import-terminated
|
||||
|
||||
public class ActorDocTest extends AbstractJavaTest {
|
||||
|
||||
public static Config config =
|
||||
ConfigFactory.parseString(
|
||||
"akka {\n"
|
||||
+ " loggers = [\"org.apache.pekko.testkit.TestEventListener\"]\n"
|
||||
+ " loglevel = \"WARNING\"\n"
|
||||
+ " stdout-loglevel = \"WARNING\"\n"
|
||||
+ "}\n");
|
||||
|
||||
static ActorSystem system = null;
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass() {
|
||||
system = ActorSystem.create("ActorDocTest", config);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterClass() {
|
||||
TestKit.shutdownActorSystem(system);
|
||||
}
|
||||
|
||||
public
|
||||
// #context-actorOf
|
||||
static class FirstActor extends AbstractActor {
|
||||
final ActorRef child = getContext().actorOf(Props.create(MyActor.class), "myChild");
|
||||
|
||||
// #plus-some-behavior
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder().matchAny(x -> getSender().tell(x, getSelf())).build();
|
||||
}
|
||||
// #plus-some-behavior
|
||||
}
|
||||
// #context-actorOf
|
||||
|
||||
public static class SomeActor extends AbstractActor {
|
||||
// #createReceive
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder().match(String.class, s -> System.out.println(s.toLowerCase())).build();
|
||||
}
|
||||
// #createReceive
|
||||
}
|
||||
|
||||
public
|
||||
// #well-structured
|
||||
static class WellStructuredActor extends AbstractActor {
|
||||
|
||||
public static class Msg1 {}
|
||||
|
||||
public static class Msg2 {}
|
||||
|
||||
public static class Msg3 {}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(Msg1.class, this::receiveMsg1)
|
||||
.match(Msg2.class, this::receiveMsg2)
|
||||
.match(Msg3.class, this::receiveMsg3)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void receiveMsg1(Msg1 msg) {
|
||||
// actual work
|
||||
}
|
||||
|
||||
private void receiveMsg2(Msg2 msg) {
|
||||
// actual work
|
||||
}
|
||||
|
||||
private void receiveMsg3(Msg3 msg) {
|
||||
// actual work
|
||||
}
|
||||
}
|
||||
// #well-structured
|
||||
|
||||
public
|
||||
// #optimized
|
||||
static class OptimizedActor extends UntypedAbstractActor {
|
||||
|
||||
public static class Msg1 {}
|
||||
|
||||
public static class Msg2 {}
|
||||
|
||||
public static class Msg3 {}
|
||||
|
||||
@Override
|
||||
public void onReceive(Object msg) throws Exception {
|
||||
if (msg instanceof Msg1) receiveMsg1((Msg1) msg);
|
||||
else if (msg instanceof Msg2) receiveMsg2((Msg2) msg);
|
||||
else if (msg instanceof Msg3) receiveMsg3((Msg3) msg);
|
||||
else unhandled(msg);
|
||||
}
|
||||
|
||||
private void receiveMsg1(Msg1 msg) {
|
||||
// actual work
|
||||
}
|
||||
|
||||
private void receiveMsg2(Msg2 msg) {
|
||||
// actual work
|
||||
}
|
||||
|
||||
private void receiveMsg3(Msg3 msg) {
|
||||
// actual work
|
||||
}
|
||||
}
|
||||
// #optimized
|
||||
|
||||
public static class ActorWithArgs extends AbstractActor {
|
||||
private final String args;
|
||||
|
||||
public ActorWithArgs(String args) {
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder().matchAny(x -> {}).build();
|
||||
}
|
||||
}
|
||||
|
||||
public
|
||||
// #props-factory
|
||||
static class DemoActor extends AbstractActor {
|
||||
/**
|
||||
* Create Props for an actor of this type.
|
||||
*
|
||||
* @param magicNumber The magic number to be passed to this actor’s constructor.
|
||||
* @return a Props for creating this actor, which can then be further configured (e.g. calling
|
||||
* `.withDispatcher()` on it)
|
||||
*/
|
||||
static Props props(Integer magicNumber) {
|
||||
// You need to specify the actual type of the returned actor
|
||||
// since Java 8 lambdas have some runtime type information erased
|
||||
return Props.create(DemoActor.class, () -> new DemoActor(magicNumber));
|
||||
}
|
||||
|
||||
private final Integer magicNumber;
|
||||
|
||||
public DemoActor(Integer magicNumber) {
|
||||
this.magicNumber = magicNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Integer.class,
|
||||
i -> {
|
||||
getSender().tell(i + magicNumber, getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// #props-factory
|
||||
public
|
||||
// #props-factory
|
||||
static class SomeOtherActor extends AbstractActor {
|
||||
// Props(new DemoActor(42)) would not be safe
|
||||
ActorRef demoActor = getContext().actorOf(DemoActor.props(42), "demo");
|
||||
// ...
|
||||
// #props-factory
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return AbstractActor.emptyBehavior();
|
||||
}
|
||||
// #props-factory
|
||||
}
|
||||
// #props-factory
|
||||
|
||||
public
|
||||
// #messages-in-companion
|
||||
static class DemoMessagesActor extends AbstractLoggingActor {
|
||||
|
||||
public static class Greeting {
|
||||
private final String from;
|
||||
|
||||
public Greeting(String from) {
|
||||
this.from = from;
|
||||
}
|
||||
|
||||
public String getGreeter() {
|
||||
return from;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Greeting.class,
|
||||
g -> {
|
||||
log().info("I was greeted by {}", g.getGreeter());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #messages-in-companion
|
||||
|
||||
public static class LifecycleMethods extends AbstractActor {
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return AbstractActor.emptyBehavior();
|
||||
}
|
||||
|
||||
/*
|
||||
* This section must be kept in sync with the actual Actor trait.
|
||||
*
|
||||
* BOYSCOUT RULE: whenever you read this, verify that!
|
||||
*/
|
||||
// #lifecycle-callbacks
|
||||
public void preStart() {}
|
||||
|
||||
public void preRestart(Throwable reason, Optional<Object> message) {
|
||||
for (ActorRef each : getContext().getChildren()) {
|
||||
getContext().unwatch(each);
|
||||
getContext().stop(each);
|
||||
}
|
||||
postStop();
|
||||
}
|
||||
|
||||
public void postRestart(Throwable reason) {
|
||||
preStart();
|
||||
}
|
||||
|
||||
public void postStop() {}
|
||||
// #lifecycle-callbacks
|
||||
|
||||
}
|
||||
|
||||
public static class Hook extends AbstractActor {
|
||||
ActorRef target = null;
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return AbstractActor.emptyBehavior();
|
||||
}
|
||||
|
||||
// #preStart
|
||||
@Override
|
||||
public void preStart() {
|
||||
target = getContext().actorOf(Props.create(MyActor.class, "target"));
|
||||
}
|
||||
// #preStart
|
||||
// #postStop
|
||||
@Override
|
||||
public void postStop() {
|
||||
// #clean-up-some-resources
|
||||
final String message = "stopped";
|
||||
// #tell
|
||||
// don’t forget to think about who is the sender (2nd argument)
|
||||
target.tell(message, getSelf());
|
||||
// #tell
|
||||
final Object result = "";
|
||||
// #forward
|
||||
target.forward(result, getContext());
|
||||
// #forward
|
||||
target = null;
|
||||
// #clean-up-some-resources
|
||||
}
|
||||
// #postStop
|
||||
|
||||
// compilation test only
|
||||
public void compileSelections() {
|
||||
// #selection-local
|
||||
// will look up this absolute path
|
||||
getContext().actorSelection("/user/serviceA/actor");
|
||||
// will look up sibling beneath same supervisor
|
||||
getContext().actorSelection("../joe");
|
||||
// #selection-local
|
||||
|
||||
// #selection-wildcard
|
||||
// will look all children to serviceB with names starting with worker
|
||||
getContext().actorSelection("/user/serviceB/worker*");
|
||||
// will look up all siblings beneath same supervisor
|
||||
getContext().actorSelection("../*");
|
||||
// #selection-wildcard
|
||||
|
||||
// #selection-remote
|
||||
getContext().actorSelection("akka://app@otherhost:1234/user/serviceB");
|
||||
// #selection-remote
|
||||
}
|
||||
}
|
||||
|
||||
public static class ReplyException extends AbstractActor {
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchAny(
|
||||
x -> {
|
||||
// #reply-exception
|
||||
try {
|
||||
String result = operation();
|
||||
getSender().tell(result, getSelf());
|
||||
} catch (Exception e) {
|
||||
getSender().tell(new org.apache.pekko.actor.Status.Failure(e), getSelf());
|
||||
throw e;
|
||||
}
|
||||
// #reply-exception
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private String operation() {
|
||||
return "Hi";
|
||||
}
|
||||
}
|
||||
|
||||
public
|
||||
// #gracefulStop-actor
|
||||
static class Manager extends AbstractActor {
|
||||
private static enum Shutdown {
|
||||
Shutdown
|
||||
}
|
||||
|
||||
public static final Shutdown SHUTDOWN = Shutdown.Shutdown;
|
||||
|
||||
private ActorRef worker =
|
||||
getContext().watch(getContext().actorOf(Props.create(Cruncher.class), "worker"));
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchEquals(
|
||||
"job",
|
||||
s -> {
|
||||
worker.tell("crunch", getSelf());
|
||||
})
|
||||
.matchEquals(
|
||||
SHUTDOWN,
|
||||
x -> {
|
||||
worker.tell(PoisonPill.getInstance(), getSelf());
|
||||
getContext().become(shuttingDown());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private AbstractActor.Receive shuttingDown() {
|
||||
return receiveBuilder()
|
||||
.matchEquals(
|
||||
"job", s -> getSender().tell("service unavailable, shutting down", getSelf()))
|
||||
.match(Terminated.class, t -> t.actor().equals(worker), t -> getContext().stop(getSelf()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #gracefulStop-actor
|
||||
|
||||
@Test
|
||||
public void usePatternsGracefulStop() throws Exception {
|
||||
ActorRef actorRef = system.actorOf(Props.create(Manager.class));
|
||||
// #gracefulStop
|
||||
try {
|
||||
CompletionStage<Boolean> stopped =
|
||||
gracefulStop(actorRef, Duration.ofSeconds(5), Manager.SHUTDOWN);
|
||||
stopped.toCompletableFuture().get(6, TimeUnit.SECONDS);
|
||||
// the actor has been stopped
|
||||
} catch (AskTimeoutException e) {
|
||||
// the actor wasn't stopped within 5 seconds
|
||||
}
|
||||
// #gracefulStop
|
||||
}
|
||||
|
||||
public static class Cruncher extends AbstractActor {
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder().matchEquals("crunch", s -> {}).build();
|
||||
}
|
||||
}
|
||||
|
||||
public
|
||||
// #swapper
|
||||
static class Swapper extends AbstractLoggingActor {
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchEquals(
|
||||
Swap,
|
||||
s -> {
|
||||
log().info("Hi");
|
||||
getContext()
|
||||
.become(
|
||||
receiveBuilder()
|
||||
.matchEquals(
|
||||
Swap,
|
||||
x -> {
|
||||
log().info("Ho");
|
||||
getContext()
|
||||
.unbecome(); // resets the latest 'become' (just for fun)
|
||||
})
|
||||
.build(),
|
||||
false); // push on top instead of replace
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// #swapper
|
||||
public
|
||||
// #swapper
|
||||
static class SwapperApp {
|
||||
public static void main(String[] args) {
|
||||
ActorSystem system = ActorSystem.create("SwapperSystem");
|
||||
ActorRef swapper = system.actorOf(Props.create(Swapper.class), "swapper");
|
||||
swapper.tell(Swap, ActorRef.noSender()); // logs Hi
|
||||
swapper.tell(Swap, ActorRef.noSender()); // logs Ho
|
||||
swapper.tell(Swap, ActorRef.noSender()); // logs Hi
|
||||
swapper.tell(Swap, ActorRef.noSender()); // logs Ho
|
||||
swapper.tell(Swap, ActorRef.noSender()); // logs Hi
|
||||
swapper.tell(Swap, ActorRef.noSender()); // logs Ho
|
||||
system.terminate();
|
||||
}
|
||||
}
|
||||
// #swapper
|
||||
|
||||
@Test
|
||||
public void creatingActorWithSystemActorOf() {
|
||||
// #system-actorOf
|
||||
// ActorSystem is a heavy object: create only one per application
|
||||
final ActorSystem system = ActorSystem.create("MySystem", config);
|
||||
final ActorRef myActor = system.actorOf(Props.create(MyActor.class), "myactor");
|
||||
// #system-actorOf
|
||||
try {
|
||||
new TestKit(system) {
|
||||
{
|
||||
myActor.tell("hello", getRef());
|
||||
expectMsgEquals("hello");
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
TestKit.shutdownActorSystem(system);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void creatingGraduallyBuiltActorWithSystemActorOf() {
|
||||
final ActorSystem system = ActorSystem.create("MySystem", config);
|
||||
final ActorRef actor =
|
||||
system.actorOf(Props.create(GraduallyBuiltActor.class), "graduallyBuiltActor");
|
||||
try {
|
||||
new TestKit(system) {
|
||||
{
|
||||
actor.tell("hello", getRef());
|
||||
expectMsgEquals("hello");
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
TestKit.shutdownActorSystem(system);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void creatingPropsConfig() {
|
||||
// #creating-props
|
||||
Props props1 = Props.create(MyActor.class);
|
||||
Props props2 =
|
||||
Props.create(ActorWithArgs.class, () -> new ActorWithArgs("arg")); // careful, see below
|
||||
Props props3 = Props.create(ActorWithArgs.class, "arg");
|
||||
// #creating-props
|
||||
|
||||
// #creating-props-deprecated
|
||||
// NOT RECOMMENDED within another actor:
|
||||
// encourages to close over enclosing class
|
||||
Props props7 = Props.create(ActorWithArgs.class, () -> new ActorWithArgs("arg"));
|
||||
// #creating-props-deprecated
|
||||
}
|
||||
|
||||
// Commented out because this 'Props.create' overload is now deprecated
|
||||
// @Test(expected = IllegalArgumentException.class)
|
||||
public void creatingPropsIllegal() {
|
||||
/*
|
||||
// #creating-props-illegal
|
||||
// This will throw an IllegalArgumentException since some runtime
|
||||
// type information of the lambda is erased.
|
||||
// Use Props.create(actorClass, Creator) instead.
|
||||
Props props = Props.create(() -> new ActorWithArgs("arg"));
|
||||
// #creating-props-illegal
|
||||
*/
|
||||
}
|
||||
|
||||
public
|
||||
// #receive-timeout
|
||||
static class ReceiveTimeoutActor extends AbstractActor {
|
||||
// #receive-timeout
|
||||
ActorRef target = getContext().getSystem().deadLetters();
|
||||
// #receive-timeout
|
||||
public ReceiveTimeoutActor() {
|
||||
// To set an initial delay
|
||||
getContext().setReceiveTimeout(Duration.ofSeconds(10));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchEquals(
|
||||
"Hello",
|
||||
s -> {
|
||||
// To set in a response to a message
|
||||
getContext().setReceiveTimeout(Duration.ofSeconds(1));
|
||||
// #receive-timeout
|
||||
target = getSender();
|
||||
target.tell("Hello world", getSelf());
|
||||
// #receive-timeout
|
||||
})
|
||||
.match(
|
||||
ReceiveTimeout.class,
|
||||
r -> {
|
||||
// To turn it off
|
||||
getContext().cancelReceiveTimeout();
|
||||
// #receive-timeout
|
||||
target.tell("timeout", getSelf());
|
||||
// #receive-timeout
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #receive-timeout
|
||||
|
||||
@Test
|
||||
public void using_receiveTimeout() {
|
||||
final ActorRef myActor = system.actorOf(Props.create(ReceiveTimeoutActor.class));
|
||||
new TestKit(system) {
|
||||
{
|
||||
myActor.tell("Hello", getRef());
|
||||
expectMsgEquals("Hello world");
|
||||
expectMsgEquals("timeout");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static class ReceiveTimeoutWithCancelActor extends AbstractActor {
|
||||
private ActorRef target = getContext().getSystem().deadLetters();
|
||||
|
||||
public ReceiveTimeoutWithCancelActor() {
|
||||
getContext().setReceiveTimeout(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchEquals(
|
||||
"Hello",
|
||||
s -> {
|
||||
target = getSender();
|
||||
target.tell("Hello world", getSelf());
|
||||
getContext().setReceiveTimeout(Duration.ofMillis(200));
|
||||
})
|
||||
.match(
|
||||
ReceiveTimeout.class,
|
||||
r -> {
|
||||
getContext().cancelReceiveTimeout();
|
||||
target.tell("timeout", getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancel_receiveTimeout() {
|
||||
final ActorRef myActor = system.actorOf(Props.create(ReceiveTimeoutWithCancelActor.class));
|
||||
new TestKit(system) {
|
||||
{
|
||||
myActor.tell("Hello", getRef());
|
||||
expectMsgEquals("Hello world");
|
||||
expectMsgEquals("timeout");
|
||||
expectNoMessage(Duration.ofMillis(400));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public
|
||||
// #hot-swap-actor
|
||||
static class HotSwapActor extends AbstractActor {
|
||||
private AbstractActor.Receive angry;
|
||||
private AbstractActor.Receive happy;
|
||||
|
||||
public HotSwapActor() {
|
||||
angry =
|
||||
receiveBuilder()
|
||||
.matchEquals(
|
||||
"foo",
|
||||
s -> {
|
||||
getSender().tell("I am already angry?", getSelf());
|
||||
})
|
||||
.matchEquals(
|
||||
"bar",
|
||||
s -> {
|
||||
getContext().become(happy);
|
||||
})
|
||||
.build();
|
||||
|
||||
happy =
|
||||
receiveBuilder()
|
||||
.matchEquals(
|
||||
"bar",
|
||||
s -> {
|
||||
getSender().tell("I am already happy :-)", getSelf());
|
||||
})
|
||||
.matchEquals(
|
||||
"foo",
|
||||
s -> {
|
||||
getContext().become(angry);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchEquals("foo", s -> getContext().become(angry))
|
||||
.matchEquals("bar", s -> getContext().become(happy))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #hot-swap-actor
|
||||
|
||||
@Test
|
||||
public void using_hot_swap() {
|
||||
final ActorRef actor = system.actorOf(Props.create(HotSwapActor.class), "hot");
|
||||
new TestKit(system) {
|
||||
{
|
||||
actor.tell("foo", getRef());
|
||||
actor.tell("foo", getRef());
|
||||
expectMsgEquals("I am already angry?");
|
||||
actor.tell("bar", getRef());
|
||||
actor.tell("bar", getRef());
|
||||
expectMsgEquals("I am already happy :-)");
|
||||
actor.tell("foo", getRef());
|
||||
actor.tell("foo", getRef());
|
||||
expectMsgEquals("I am already angry?");
|
||||
expectNoMessage(Duration.ofSeconds(1));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public
|
||||
// #stash
|
||||
static class ActorWithProtocol extends AbstractActorWithStash {
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchEquals(
|
||||
"open",
|
||||
s -> {
|
||||
getContext()
|
||||
.become(
|
||||
receiveBuilder()
|
||||
.matchEquals(
|
||||
"write",
|
||||
ws -> {
|
||||
/* do writing */
|
||||
})
|
||||
.matchEquals(
|
||||
"close",
|
||||
cs -> {
|
||||
unstashAll();
|
||||
getContext().unbecome();
|
||||
})
|
||||
.matchAny(msg -> stash())
|
||||
.build(),
|
||||
false);
|
||||
})
|
||||
.matchAny(msg -> stash())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #stash
|
||||
|
||||
@Test
|
||||
public void using_Stash() {
|
||||
final ActorRef actor = system.actorOf(Props.create(ActorWithProtocol.class), "stash");
|
||||
}
|
||||
|
||||
public
|
||||
// #watch
|
||||
static class WatchActor extends AbstractActor {
|
||||
private final ActorRef child = getContext().actorOf(Props.empty(), "target");
|
||||
private ActorRef lastSender = system.deadLetters();
|
||||
|
||||
public WatchActor() {
|
||||
getContext().watch(child); // <-- this is the only call needed for registration
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchEquals(
|
||||
"kill",
|
||||
s -> {
|
||||
getContext().stop(child);
|
||||
lastSender = getSender();
|
||||
})
|
||||
.match(
|
||||
Terminated.class,
|
||||
t -> t.actor().equals(child),
|
||||
t -> {
|
||||
lastSender.tell("finished", getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #watch
|
||||
|
||||
@Test
|
||||
public void using_watch() {
|
||||
ActorRef actor = system.actorOf(Props.create(WatchActor.class));
|
||||
|
||||
new TestKit(system) {
|
||||
{
|
||||
actor.tell("kill", getRef());
|
||||
expectMsgEquals("finished");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public
|
||||
// #identify
|
||||
static class Follower extends AbstractActor {
|
||||
final Integer identifyId = 1;
|
||||
|
||||
public Follower() {
|
||||
ActorSelection selection = getContext().actorSelection("/user/another");
|
||||
selection.tell(new Identify(identifyId), getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
ActorIdentity.class,
|
||||
id -> id.getActorRef().isPresent(),
|
||||
id -> {
|
||||
ActorRef ref = id.getActorRef().get();
|
||||
getContext().watch(ref);
|
||||
getContext().become(active(ref));
|
||||
})
|
||||
.match(
|
||||
ActorIdentity.class,
|
||||
id -> !id.getActorRef().isPresent(),
|
||||
id -> {
|
||||
getContext().stop(getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
final AbstractActor.Receive active(final ActorRef another) {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Terminated.class, t -> t.actor().equals(another), t -> getContext().stop(getSelf()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #identify
|
||||
|
||||
@Test
|
||||
public void using_Identify() {
|
||||
ActorRef a = system.actorOf(Props.empty());
|
||||
ActorRef b = system.actorOf(Props.create(Follower.class));
|
||||
|
||||
new TestKit(system) {
|
||||
{
|
||||
watch(b);
|
||||
system.stop(a);
|
||||
assertEquals(expectMsgClass(Duration.ofSeconds(2), Terminated.class).actor(), b);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usePatternsAskPipe() {
|
||||
new TestKit(system) {
|
||||
{
|
||||
ActorRef actorA = system.actorOf(TestActors.echoActorProps());
|
||||
ActorRef actorB = system.actorOf(TestActors.echoActorProps());
|
||||
ActorRef actorC = getRef();
|
||||
|
||||
// #ask-pipe
|
||||
final Duration t = Duration.ofSeconds(5);
|
||||
|
||||
// using 1000ms timeout
|
||||
CompletableFuture<Object> future1 =
|
||||
ask(actorA, "request", Duration.ofMillis(1000)).toCompletableFuture();
|
||||
|
||||
// using timeout from above
|
||||
CompletableFuture<Object> future2 = ask(actorB, "another request", t).toCompletableFuture();
|
||||
|
||||
CompletableFuture<Result> transformed =
|
||||
future1.thenCombine(future2, (x, s) -> new Result((String) x, (String) s));
|
||||
|
||||
pipe(transformed, system.dispatcher()).to(actorC);
|
||||
// #ask-pipe
|
||||
|
||||
expectMsgEquals(new Result("request", "another request"));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void useKill() {
|
||||
new TestKit(system) {
|
||||
{
|
||||
ActorRef victim = system.actorOf(TestActors.echoActorProps());
|
||||
watch(victim);
|
||||
// #kill
|
||||
victim.tell(org.apache.pekko.actor.Kill.getInstance(), ActorRef.noSender());
|
||||
|
||||
// expecting the actor to indeed terminate:
|
||||
expectTerminated(Duration.ofSeconds(3), victim);
|
||||
// #kill
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usePoisonPill() {
|
||||
new TestKit(system) {
|
||||
{
|
||||
ActorRef victim = system.actorOf(TestActors.echoActorProps());
|
||||
watch(victim);
|
||||
// #poison-pill
|
||||
victim.tell(org.apache.pekko.actor.PoisonPill.getInstance(), ActorRef.noSender());
|
||||
// #poison-pill
|
||||
expectTerminated(Duration.ofSeconds(3), victim);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coordinatedShutdownActorTermination() {
|
||||
ActorRef someActor = system.actorOf(Props.create(FirstActor.class));
|
||||
someActor.tell(PoisonPill.getInstance(), ActorRef.noSender());
|
||||
// https://github.com/akka/akka/issues/29056
|
||||
// #coordinated-shutdown-addActorTerminationTask
|
||||
CoordinatedShutdown.get(system)
|
||||
.addActorTerminationTask(
|
||||
CoordinatedShutdown.PhaseBeforeServiceUnbind(),
|
||||
"someTaskName",
|
||||
someActor,
|
||||
Optional.of("stop"));
|
||||
// #coordinated-shutdown-addActorTerminationTask
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (C) 2016-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
// #bytebufserializer-with-manifest
|
||||
import org.apache.pekko.serialization.ByteBufferSerializer;
|
||||
import org.apache.pekko.serialization.SerializerWithStringManifest;
|
||||
|
||||
// #bytebufserializer-with-manifest
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ByteBufferSerializerDocTest {
|
||||
|
||||
static // #bytebufserializer-with-manifest
|
||||
class ExampleByteBufSerializer extends SerializerWithStringManifest
|
||||
implements ByteBufferSerializer {
|
||||
|
||||
@Override
|
||||
public int identifier() {
|
||||
return 1337;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String manifest(Object o) {
|
||||
return "serialized-" + o.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBinary(Object o) {
|
||||
// in production code, acquire this from a BufferPool
|
||||
final ByteBuffer buf = ByteBuffer.allocate(256);
|
||||
|
||||
toBinary(o, buf);
|
||||
buf.flip();
|
||||
final byte[] bytes = new byte[buf.remaining()];
|
||||
buf.get(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromBinary(byte[] bytes, String manifest) {
|
||||
return fromBinary(ByteBuffer.wrap(bytes), manifest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toBinary(Object o, ByteBuffer buf) {
|
||||
// Implement actual serialization here
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromBinary(ByteBuffer buf, String manifest) {
|
||||
// Implement actual deserialization here
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// #bytebufserializer-with-manifest
|
||||
|
||||
static class OnlyForDocInclude {
|
||||
static
|
||||
// #ByteBufferSerializer-interface
|
||||
interface ByteBufferSerializer {
|
||||
/** Serializes the given object into the `ByteBuffer`. */
|
||||
void toBinary(Object o, ByteBuffer buf);
|
||||
|
||||
/**
|
||||
* Produces an object from a `ByteBuffer`, with an optional type-hint; the class should be
|
||||
* loaded using ActorSystem.dynamicAccess.
|
||||
*/
|
||||
Object fromBinary(ByteBuffer buf, String manifest);
|
||||
}
|
||||
// #ByteBufferSerializer-interface
|
||||
}
|
||||
}
|
||||
106
docs/src/test/java/jdocs/actor/DependencyInjectionDocTest.java
Normal file
106
docs/src/test/java/jdocs/actor/DependencyInjectionDocTest.java
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.Props;
|
||||
|
||||
// #import
|
||||
import org.apache.pekko.actor.Actor;
|
||||
import org.apache.pekko.actor.IndirectActorProducer;
|
||||
// #import
|
||||
|
||||
public class DependencyInjectionDocTest extends AbstractJavaTest {
|
||||
|
||||
public static class TheActor extends AbstractActor {
|
||||
|
||||
final String s;
|
||||
|
||||
public TheActor(String s) {
|
||||
this.s = s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
msg -> {
|
||||
getSender().tell(s, getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
static ActorSystem system = null;
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass() {
|
||||
system = ActorSystem.create("DependencyInjectionDocTest");
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterClass() {
|
||||
TestKit.shutdownActorSystem(system);
|
||||
}
|
||||
|
||||
// this is just to make the test below a tiny fraction nicer
|
||||
private ActorSystem getContext() {
|
||||
return system;
|
||||
}
|
||||
|
||||
static
|
||||
// #creating-indirectly
|
||||
class DependencyInjector implements IndirectActorProducer {
|
||||
final Object applicationContext;
|
||||
final String beanName;
|
||||
|
||||
public DependencyInjector(Object applicationContext, String beanName) {
|
||||
this.applicationContext = applicationContext;
|
||||
this.beanName = beanName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Actor> actorClass() {
|
||||
return TheActor.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TheActor produce() {
|
||||
TheActor result;
|
||||
// #obtain-fresh-Actor-instance-from-DI-framework
|
||||
result = new TheActor((String) applicationContext);
|
||||
// #obtain-fresh-Actor-instance-from-DI-framework
|
||||
return result;
|
||||
}
|
||||
}
|
||||
// #creating-indirectly
|
||||
|
||||
@Test
|
||||
public void indirectActorOf() {
|
||||
final String applicationContext = "...";
|
||||
// #creating-indirectly
|
||||
|
||||
final ActorRef myActor =
|
||||
getContext()
|
||||
.actorOf(
|
||||
Props.create(DependencyInjector.class, applicationContext, "TheActor"), "TheActor");
|
||||
// #creating-indirectly
|
||||
new TestKit(system) {
|
||||
{
|
||||
myActor.tell("hello", getRef());
|
||||
expectMsgEquals("...");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
528
docs/src/test/java/jdocs/actor/FaultHandlingDocSample.java
Normal file
528
docs/src/test/java/jdocs/actor/FaultHandlingDocSample.java
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
// #all
|
||||
// #imports
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.apache.pekko.actor.*;
|
||||
import org.apache.pekko.dispatch.Mapper;
|
||||
import org.apache.pekko.event.LoggingReceive;
|
||||
import org.apache.pekko.japi.pf.DeciderBuilder;
|
||||
import org.apache.pekko.pattern.Patterns;
|
||||
import org.apache.pekko.util.Timeout;
|
||||
import com.typesafe.config.Config;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
|
||||
import static org.apache.pekko.japi.Util.classTag;
|
||||
import static org.apache.pekko.actor.SupervisorStrategy.restart;
|
||||
import static org.apache.pekko.actor.SupervisorStrategy.stop;
|
||||
import static org.apache.pekko.actor.SupervisorStrategy.escalate;
|
||||
|
||||
import static org.apache.pekko.pattern.Patterns.pipe;
|
||||
|
||||
import static jdocs.actor.FaultHandlingDocSample.WorkerApi.*;
|
||||
import static jdocs.actor.FaultHandlingDocSample.CounterServiceApi.*;
|
||||
import static jdocs.actor.FaultHandlingDocSample.CounterApi.*;
|
||||
import static jdocs.actor.FaultHandlingDocSample.StorageApi.*;
|
||||
|
||||
// #imports
|
||||
|
||||
public class FaultHandlingDocSample {
|
||||
|
||||
/** Runs the sample */
|
||||
public static void main(String[] args) {
|
||||
Config config =
|
||||
ConfigFactory.parseString(
|
||||
"akka.loglevel = \"DEBUG\"\n"
|
||||
+ "akka.actor.debug {\n"
|
||||
+ " receive = on\n"
|
||||
+ " lifecycle = on\n"
|
||||
+ "}\n");
|
||||
|
||||
ActorSystem system = ActorSystem.create("FaultToleranceSample", config);
|
||||
ActorRef worker = system.actorOf(Props.create(Worker.class), "worker");
|
||||
ActorRef listener = system.actorOf(Props.create(Listener.class), "listener");
|
||||
// start the work and listen on progress
|
||||
// note that the listener is used as sender of the tell,
|
||||
// i.e. it will receive replies from the worker
|
||||
worker.tell(Start, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens on progress from the worker and shuts down the system when enough work has been done.
|
||||
*/
|
||||
public static class Listener extends AbstractLoggingActor {
|
||||
|
||||
@Override
|
||||
public void preStart() {
|
||||
// If we don't get any progress within 15 seconds then the service
|
||||
// is unavailable
|
||||
getContext().setReceiveTimeout(Duration.ofSeconds(15));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return LoggingReceive.create(
|
||||
receiveBuilder()
|
||||
.match(
|
||||
Progress.class,
|
||||
progress -> {
|
||||
log().info("Current progress: {} %", progress.percent);
|
||||
if (progress.percent >= 100.0) {
|
||||
log().info("That's all, shutting down");
|
||||
getContext().getSystem().terminate();
|
||||
}
|
||||
})
|
||||
.matchEquals(
|
||||
ReceiveTimeout.getInstance(),
|
||||
x -> {
|
||||
// No progress within 15 seconds, ServiceUnavailable
|
||||
log().error("Shutting down due to unavailable service");
|
||||
getContext().getSystem().terminate();
|
||||
})
|
||||
.build(),
|
||||
getContext());
|
||||
}
|
||||
}
|
||||
|
||||
// #messages
|
||||
public interface WorkerApi {
|
||||
public static final Object Start = "Start";
|
||||
public static final Object Do = "Do";
|
||||
|
||||
public static class Progress {
|
||||
public final double percent;
|
||||
|
||||
public Progress(double percent) {
|
||||
this.percent = percent;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("%s(%s)", getClass().getSimpleName(), percent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #messages
|
||||
|
||||
/**
|
||||
* Worker performs some work when it receives the Start message. It will continuously notify the
|
||||
* sender of the Start message of current Progress. The Worker supervise the CounterService.
|
||||
*/
|
||||
public static class Worker extends AbstractLoggingActor {
|
||||
final Timeout askTimeout = Timeout.create(Duration.ofSeconds(5));
|
||||
|
||||
// The sender of the initial Start message will continuously be notified
|
||||
// about progress
|
||||
ActorRef progressListener;
|
||||
final ActorRef counterService =
|
||||
getContext().actorOf(Props.create(CounterService.class), "counter");
|
||||
final int totalCount = 51;
|
||||
|
||||
// Stop the CounterService child if it throws ServiceUnavailable
|
||||
private static final SupervisorStrategy strategy =
|
||||
new OneForOneStrategy(
|
||||
DeciderBuilder.match(ServiceUnavailable.class, e -> stop())
|
||||
.matchAny(o -> escalate())
|
||||
.build());
|
||||
|
||||
@Override
|
||||
public SupervisorStrategy supervisorStrategy() {
|
||||
return strategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return LoggingReceive.create(
|
||||
receiveBuilder()
|
||||
.matchEquals(
|
||||
Start,
|
||||
x -> progressListener == null,
|
||||
x -> {
|
||||
progressListener = getSender();
|
||||
getContext()
|
||||
.getSystem()
|
||||
.scheduler()
|
||||
.scheduleWithFixedDelay(
|
||||
Duration.ZERO,
|
||||
Duration.ofSeconds(1L),
|
||||
getSelf(),
|
||||
Do,
|
||||
getContext().getDispatcher(),
|
||||
null);
|
||||
})
|
||||
.matchEquals(
|
||||
Do,
|
||||
x -> {
|
||||
counterService.tell(new Increment(1), getSelf());
|
||||
counterService.tell(new Increment(1), getSelf());
|
||||
counterService.tell(new Increment(1), getSelf());
|
||||
// Send current progress to the initial sender
|
||||
pipe(
|
||||
Patterns.ask(counterService, GetCurrentCount, askTimeout)
|
||||
.mapTo(classTag(CurrentCount.class))
|
||||
.map(
|
||||
new Mapper<CurrentCount, Progress>() {
|
||||
public Progress apply(CurrentCount c) {
|
||||
return new Progress(100.0 * c.count / totalCount);
|
||||
}
|
||||
},
|
||||
getContext().dispatcher()),
|
||||
getContext().dispatcher())
|
||||
.to(progressListener);
|
||||
})
|
||||
.build(),
|
||||
getContext());
|
||||
}
|
||||
}
|
||||
|
||||
// #messages
|
||||
public interface CounterServiceApi {
|
||||
|
||||
public static final Object GetCurrentCount = "GetCurrentCount";
|
||||
|
||||
public static class CurrentCount {
|
||||
public final String key;
|
||||
public final long count;
|
||||
|
||||
public CurrentCount(String key, long count) {
|
||||
this.key = key;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("%s(%s, %s)", getClass().getSimpleName(), key, count);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Increment {
|
||||
public final long n;
|
||||
|
||||
public Increment(long n) {
|
||||
this.n = n;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("%s(%s)", getClass().getSimpleName(), n);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ServiceUnavailable extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ServiceUnavailable(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #messages
|
||||
|
||||
/**
|
||||
* Adds the value received in Increment message to a persistent counter. Replies with CurrentCount
|
||||
* when it is asked for CurrentCount. CounterService supervise Storage and Counter.
|
||||
*/
|
||||
public static class CounterService extends AbstractLoggingActor {
|
||||
|
||||
// Reconnect message
|
||||
static final Object Reconnect = "Reconnect";
|
||||
|
||||
private static class SenderMsgPair {
|
||||
final ActorRef sender;
|
||||
final Object msg;
|
||||
|
||||
SenderMsgPair(ActorRef sender, Object msg) {
|
||||
this.msg = msg;
|
||||
this.sender = sender;
|
||||
}
|
||||
}
|
||||
|
||||
final String key = getSelf().path().name();
|
||||
ActorRef storage;
|
||||
ActorRef counter;
|
||||
final List<SenderMsgPair> backlog = new ArrayList<>();
|
||||
final int MAX_BACKLOG = 10000;
|
||||
|
||||
// Restart the storage child when StorageException is thrown.
|
||||
// After 3 restarts within 5 seconds it will be stopped.
|
||||
private static final SupervisorStrategy strategy =
|
||||
new OneForOneStrategy(
|
||||
3,
|
||||
Duration.ofSeconds(5),
|
||||
DeciderBuilder.match(StorageException.class, e -> restart())
|
||||
.matchAny(o -> escalate())
|
||||
.build());
|
||||
|
||||
@Override
|
||||
public SupervisorStrategy supervisorStrategy() {
|
||||
return strategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preStart() {
|
||||
initStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* The child storage is restarted in case of failure, but after 3 restarts, and still failing it
|
||||
* will be stopped. Better to back-off than continuously failing. When it has been stopped we
|
||||
* will schedule a Reconnect after a delay. Watch the child so we receive Terminated message
|
||||
* when it has been terminated.
|
||||
*/
|
||||
void initStorage() {
|
||||
storage = getContext().watch(getContext().actorOf(Props.create(Storage.class), "storage"));
|
||||
// Tell the counter, if any, to use the new storage
|
||||
if (counter != null) counter.tell(new UseStorage(storage), getSelf());
|
||||
// We need the initial value to be able to operate
|
||||
storage.tell(new Get(key), getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return LoggingReceive.create(
|
||||
receiveBuilder()
|
||||
.match(
|
||||
Entry.class,
|
||||
entry -> entry.key.equals(key) && counter == null,
|
||||
entry -> {
|
||||
// Reply from Storage of the initial value, now we can create the Counter
|
||||
final long value = entry.value;
|
||||
counter = getContext().actorOf(Props.create(Counter.class, key, value));
|
||||
// Tell the counter to use current storage
|
||||
counter.tell(new UseStorage(storage), getSelf());
|
||||
// and send the buffered backlog to the counter
|
||||
for (SenderMsgPair each : backlog) {
|
||||
counter.tell(each.msg, each.sender);
|
||||
}
|
||||
backlog.clear();
|
||||
})
|
||||
.match(
|
||||
Increment.class,
|
||||
increment -> {
|
||||
forwardOrPlaceInBacklog(increment);
|
||||
})
|
||||
.matchEquals(
|
||||
GetCurrentCount,
|
||||
gcc -> {
|
||||
forwardOrPlaceInBacklog(gcc);
|
||||
})
|
||||
.match(
|
||||
Terminated.class,
|
||||
o -> {
|
||||
// After 3 restarts the storage child is stopped.
|
||||
// We receive Terminated because we watch the child, see initStorage.
|
||||
storage = null;
|
||||
// Tell the counter that there is no storage for the moment
|
||||
counter.tell(new UseStorage(null), getSelf());
|
||||
// Try to re-establish storage after while
|
||||
getContext()
|
||||
.getSystem()
|
||||
.scheduler()
|
||||
.scheduleOnce(
|
||||
Duration.ofSeconds(10),
|
||||
getSelf(),
|
||||
Reconnect,
|
||||
getContext().getDispatcher(),
|
||||
null);
|
||||
})
|
||||
.matchEquals(
|
||||
Reconnect,
|
||||
o -> {
|
||||
// Re-establish storage after the scheduled delay
|
||||
initStorage();
|
||||
})
|
||||
.build(),
|
||||
getContext());
|
||||
}
|
||||
|
||||
void forwardOrPlaceInBacklog(Object msg) {
|
||||
// We need the initial value from storage before we can start delegate to
|
||||
// the counter. Before that we place the messages in a backlog, to be sent
|
||||
// to the counter when it is initialized.
|
||||
if (counter == null) {
|
||||
if (backlog.size() >= MAX_BACKLOG)
|
||||
throw new ServiceUnavailable("CounterService not available," + " lack of initial value");
|
||||
backlog.add(new SenderMsgPair(getSender(), msg));
|
||||
} else {
|
||||
counter.forward(msg, getContext());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #messages
|
||||
public interface CounterApi {
|
||||
public static class UseStorage {
|
||||
public final ActorRef storage;
|
||||
|
||||
public UseStorage(ActorRef storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("%s(%s)", getClass().getSimpleName(), storage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #messages
|
||||
|
||||
/**
|
||||
* The in memory count variable that will send current value to the Storage, if there is any
|
||||
* storage available at the moment.
|
||||
*/
|
||||
public static class Counter extends AbstractLoggingActor {
|
||||
final String key;
|
||||
long count;
|
||||
ActorRef storage;
|
||||
|
||||
public Counter(String key, long initialValue) {
|
||||
this.key = key;
|
||||
this.count = initialValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return LoggingReceive.create(
|
||||
receiveBuilder()
|
||||
.match(
|
||||
UseStorage.class,
|
||||
useStorage -> {
|
||||
storage = useStorage.storage;
|
||||
storeCount();
|
||||
})
|
||||
.match(
|
||||
Increment.class,
|
||||
increment -> {
|
||||
count += increment.n;
|
||||
storeCount();
|
||||
})
|
||||
.matchEquals(
|
||||
GetCurrentCount,
|
||||
gcc -> {
|
||||
getSender().tell(new CurrentCount(key, count), getSelf());
|
||||
})
|
||||
.build(),
|
||||
getContext());
|
||||
}
|
||||
|
||||
void storeCount() {
|
||||
// Delegate dangerous work, to protect our valuable state.
|
||||
// We can continue without storage.
|
||||
if (storage != null) {
|
||||
storage.tell(new Store(new Entry(key, count)), getSelf());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #messages
|
||||
public interface StorageApi {
|
||||
|
||||
public static class Store {
|
||||
public final Entry entry;
|
||||
|
||||
public Store(Entry entry) {
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("%s(%s)", getClass().getSimpleName(), entry);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Entry {
|
||||
public final String key;
|
||||
public final long value;
|
||||
|
||||
public Entry(String key, long value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("%s(%s, %s)", getClass().getSimpleName(), key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Get {
|
||||
public final String key;
|
||||
|
||||
public Get(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("%s(%s)", getClass().getSimpleName(), key);
|
||||
}
|
||||
}
|
||||
|
||||
public static class StorageException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public StorageException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #messages
|
||||
|
||||
/**
|
||||
* Saves key/value pairs to persistent storage when receiving Store message. Replies with current
|
||||
* value when receiving Get message. Will throw StorageException if the underlying data store is
|
||||
* out of order.
|
||||
*/
|
||||
public static class Storage extends AbstractLoggingActor {
|
||||
|
||||
final DummyDB db = DummyDB.instance;
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return LoggingReceive.create(
|
||||
receiveBuilder()
|
||||
.match(
|
||||
Store.class,
|
||||
store -> {
|
||||
db.save(store.entry.key, store.entry.value);
|
||||
})
|
||||
.match(
|
||||
Get.class,
|
||||
get -> {
|
||||
Long value = db.load(get.key);
|
||||
getSender()
|
||||
.tell(
|
||||
new Entry(get.key, value == null ? Long.valueOf(0L) : value),
|
||||
getSelf());
|
||||
})
|
||||
.build(),
|
||||
getContext());
|
||||
}
|
||||
}
|
||||
|
||||
// #dummydb
|
||||
public static class DummyDB {
|
||||
public static final DummyDB instance = new DummyDB();
|
||||
private final Map<String, Long> db = new HashMap<String, Long>();
|
||||
|
||||
private DummyDB() {}
|
||||
|
||||
public synchronized void save(String key, Long value) throws StorageException {
|
||||
if (11 <= value && value <= 14)
|
||||
throw new StorageException("Simulated store failure " + value);
|
||||
db.put(key, value);
|
||||
}
|
||||
|
||||
public synchronized Long load(String key) throws StorageException {
|
||||
return db.get(key);
|
||||
}
|
||||
}
|
||||
// #dummydb
|
||||
}
|
||||
// #all
|
||||
224
docs/src/test/java/jdocs/actor/FaultHandlingTest.java
Normal file
224
docs/src/test/java/jdocs/actor/FaultHandlingTest.java
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
import org.apache.pekko.actor.*;
|
||||
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import com.typesafe.config.Config;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
import jdocs.AbstractJavaTest;
|
||||
import java.util.Optional;
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.apache.pekko.pattern.Patterns.ask;
|
||||
|
||||
// #testkit
|
||||
import org.apache.pekko.testkit.TestProbe;
|
||||
import org.apache.pekko.testkit.ErrorFilter;
|
||||
import org.apache.pekko.testkit.EventFilter;
|
||||
import org.apache.pekko.testkit.TestEvent;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.apache.pekko.japi.Util.immutableSeq;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import scala.concurrent.Await;
|
||||
|
||||
// #testkit
|
||||
|
||||
// #supervisor
|
||||
import org.apache.pekko.japi.pf.DeciderBuilder;
|
||||
import org.apache.pekko.actor.SupervisorStrategy;
|
||||
|
||||
// #supervisor
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.AfterClass;
|
||||
|
||||
// #testkit
|
||||
public class FaultHandlingTest extends AbstractJavaTest {
|
||||
// #testkit
|
||||
|
||||
public static Config config =
|
||||
ConfigFactory.parseString(
|
||||
"akka {\n"
|
||||
+ " loggers = [\"org.apache.pekko.testkit.TestEventListener\"]\n"
|
||||
+ " loglevel = \"WARNING\"\n"
|
||||
+ " stdout-loglevel = \"WARNING\"\n"
|
||||
+ "}\n");
|
||||
|
||||
public
|
||||
// #supervisor
|
||||
static class Supervisor extends AbstractActor {
|
||||
|
||||
// #strategy
|
||||
private static SupervisorStrategy strategy =
|
||||
new OneForOneStrategy(
|
||||
10,
|
||||
Duration.ofMinutes(1),
|
||||
DeciderBuilder.match(ArithmeticException.class, e -> SupervisorStrategy.resume())
|
||||
.match(NullPointerException.class, e -> SupervisorStrategy.restart())
|
||||
.match(IllegalArgumentException.class, e -> SupervisorStrategy.stop())
|
||||
.matchAny(o -> SupervisorStrategy.escalate())
|
||||
.build());
|
||||
|
||||
@Override
|
||||
public SupervisorStrategy supervisorStrategy() {
|
||||
return strategy;
|
||||
}
|
||||
|
||||
// #strategy
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Props.class,
|
||||
props -> {
|
||||
getSender().tell(getContext().actorOf(props), getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// #supervisor
|
||||
|
||||
public
|
||||
// #supervisor2
|
||||
static class Supervisor2 extends AbstractActor {
|
||||
|
||||
// #strategy2
|
||||
private static SupervisorStrategy strategy =
|
||||
new OneForOneStrategy(
|
||||
10,
|
||||
Duration.ofMinutes(1),
|
||||
DeciderBuilder.match(ArithmeticException.class, e -> SupervisorStrategy.resume())
|
||||
.match(NullPointerException.class, e -> SupervisorStrategy.restart())
|
||||
.match(IllegalArgumentException.class, e -> SupervisorStrategy.stop())
|
||||
.matchAny(o -> SupervisorStrategy.escalate())
|
||||
.build());
|
||||
|
||||
@Override
|
||||
public SupervisorStrategy supervisorStrategy() {
|
||||
return strategy;
|
||||
}
|
||||
|
||||
// #strategy2
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Props.class,
|
||||
props -> {
|
||||
getSender().tell(getContext().actorOf(props), getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preRestart(Throwable cause, Optional<Object> msg) {
|
||||
// do not kill all children, which is the default here
|
||||
}
|
||||
}
|
||||
|
||||
// #supervisor2
|
||||
|
||||
public
|
||||
// #child
|
||||
static class Child extends AbstractActor {
|
||||
int state = 0;
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Exception.class,
|
||||
exception -> {
|
||||
throw exception;
|
||||
})
|
||||
.match(Integer.class, i -> state = i)
|
||||
.matchEquals("get", s -> getSender().tell(state, getSelf()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// #child
|
||||
|
||||
// #testkit
|
||||
static ActorSystem system;
|
||||
scala.concurrent.duration.Duration timeout =
|
||||
scala.concurrent.duration.Duration.create(5, SECONDS);
|
||||
|
||||
@BeforeClass
|
||||
public static void start() {
|
||||
system = ActorSystem.create("FaultHandlingTest", config);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void cleanup() {
|
||||
TestKit.shutdownActorSystem(system);
|
||||
system = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mustEmploySupervisorStrategy() throws Exception {
|
||||
// code here
|
||||
// #testkit
|
||||
EventFilter ex1 = new ErrorFilter(ArithmeticException.class);
|
||||
EventFilter ex2 = new ErrorFilter(NullPointerException.class);
|
||||
EventFilter ex3 = new ErrorFilter(IllegalArgumentException.class);
|
||||
EventFilter ex4 = new ErrorFilter(Exception.class);
|
||||
EventFilter[] ignoreExceptions = {ex1, ex2, ex3, ex4};
|
||||
system.getEventStream().publish(new TestEvent.Mute(immutableSeq(ignoreExceptions)));
|
||||
|
||||
// #create
|
||||
Props superprops = Props.create(Supervisor.class);
|
||||
ActorRef supervisor = system.actorOf(superprops, "supervisor");
|
||||
ActorRef child =
|
||||
(ActorRef) Await.result(ask(supervisor, Props.create(Child.class), 5000), timeout);
|
||||
// #create
|
||||
|
||||
// #resume
|
||||
child.tell(42, ActorRef.noSender());
|
||||
assertEquals(42, Await.result(ask(child, "get", 5000), timeout));
|
||||
child.tell(new ArithmeticException(), ActorRef.noSender());
|
||||
assertEquals(42, Await.result(ask(child, "get", 5000), timeout));
|
||||
// #resume
|
||||
|
||||
// #restart
|
||||
child.tell(new NullPointerException(), ActorRef.noSender());
|
||||
assertEquals(0, Await.result(ask(child, "get", 5000), timeout));
|
||||
// #restart
|
||||
|
||||
// #stop
|
||||
final TestProbe probe = new TestProbe(system);
|
||||
probe.watch(child);
|
||||
child.tell(new IllegalArgumentException(), ActorRef.noSender());
|
||||
probe.expectMsgClass(Terminated.class);
|
||||
// #stop
|
||||
|
||||
// #escalate-kill
|
||||
child = (ActorRef) Await.result(ask(supervisor, Props.create(Child.class), 5000), timeout);
|
||||
probe.watch(child);
|
||||
assertEquals(0, Await.result(ask(child, "get", 5000), timeout));
|
||||
child.tell(new Exception(), ActorRef.noSender());
|
||||
probe.expectMsgClass(Terminated.class);
|
||||
// #escalate-kill
|
||||
|
||||
// #escalate-restart
|
||||
superprops = Props.create(Supervisor2.class);
|
||||
supervisor = system.actorOf(superprops);
|
||||
child = (ActorRef) Await.result(ask(supervisor, Props.create(Child.class), 5000), timeout);
|
||||
child.tell(23, ActorRef.noSender());
|
||||
assertEquals(23, Await.result(ask(child, "get", 5000), timeout));
|
||||
child.tell(new Exception(), ActorRef.noSender());
|
||||
assertEquals(0, Await.result(ask(child, "get", 5000), timeout));
|
||||
// #escalate-restart
|
||||
// #testkit
|
||||
}
|
||||
}
|
||||
// #testkit
|
||||
41
docs/src/test/java/jdocs/actor/GraduallyBuiltActor.java
Normal file
41
docs/src/test/java/jdocs/actor/GraduallyBuiltActor.java
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
// #imports
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.event.Logging;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
import org.apache.pekko.japi.pf.ReceiveBuilder;
|
||||
|
||||
// #imports
|
||||
|
||||
// #actor
|
||||
public class GraduallyBuiltActor extends AbstractActor {
|
||||
private final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
ReceiveBuilder builder = ReceiveBuilder.create();
|
||||
|
||||
builder.match(
|
||||
String.class,
|
||||
s -> {
|
||||
log.info("Received String message: {}", s);
|
||||
// #actor
|
||||
// #reply
|
||||
getSender().tell(s, getSelf());
|
||||
// #reply
|
||||
// #actor
|
||||
});
|
||||
|
||||
// do some other stuff in between
|
||||
|
||||
builder.matchAny(o -> log.info("received unknown message"));
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
// #actor
|
||||
29
docs/src/test/java/jdocs/actor/ImmutableMessage.java
Normal file
29
docs/src/test/java/jdocs/actor/ImmutableMessage.java
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
// #immutable-message
|
||||
public class ImmutableMessage {
|
||||
private final int sequenceNumber;
|
||||
private final List<String> values;
|
||||
|
||||
public ImmutableMessage(int sequenceNumber, List<String> values) {
|
||||
this.sequenceNumber = sequenceNumber;
|
||||
this.values = Collections.unmodifiableList(new ArrayList<String>(values));
|
||||
}
|
||||
|
||||
public int getSequenceNumber() {
|
||||
return sequenceNumber;
|
||||
}
|
||||
|
||||
public List<String> getValues() {
|
||||
return values;
|
||||
}
|
||||
}
|
||||
// #immutable-message
|
||||
217
docs/src/test/java/jdocs/actor/InitializationDocTest.java
Normal file
217
docs/src/test/java/jdocs/actor/InitializationDocTest.java
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.japi.pf.FI;
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
|
||||
public class InitializationDocTest extends AbstractJavaTest {
|
||||
|
||||
static ActorSystem system = null;
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass() {
|
||||
system = ActorSystem.create("InitializationDocTest");
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterClass() {
|
||||
TestKit.shutdownActorSystem(system);
|
||||
}
|
||||
|
||||
public static class PreStartInitExample extends AbstractActor {
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return AbstractActor.emptyBehavior();
|
||||
}
|
||||
|
||||
// #preStartInit
|
||||
@Override
|
||||
public void preStart() {
|
||||
// Initialize children here
|
||||
}
|
||||
|
||||
// Overriding postRestart to disable the call to preStart()
|
||||
// after restarts
|
||||
@Override
|
||||
public void postRestart(Throwable reason) {}
|
||||
|
||||
// The default implementation of preRestart() stops all the children
|
||||
// of the actor. To opt-out from stopping the children, we
|
||||
// have to override preRestart()
|
||||
@Override
|
||||
public void preRestart(Throwable reason, Optional<Object> message) throws Exception {
|
||||
// Keep the call to postStop(), but no stopping of children
|
||||
postStop();
|
||||
}
|
||||
// #preStartInit
|
||||
|
||||
}
|
||||
|
||||
public static class MessageInitExample extends AbstractActor {
|
||||
private String initializeMe = null;
|
||||
|
||||
// #messageInit
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchEquals(
|
||||
"init",
|
||||
m1 -> {
|
||||
initializeMe = "Up and running";
|
||||
getContext()
|
||||
.become(
|
||||
receiveBuilder()
|
||||
.matchEquals(
|
||||
"U OK?",
|
||||
m2 -> {
|
||||
getSender().tell(initializeMe, getSelf());
|
||||
})
|
||||
.build());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
// #messageInit
|
||||
}
|
||||
|
||||
public class GenericMessage<T> {
|
||||
T value;
|
||||
|
||||
public GenericMessage(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GenericActor extends AbstractActor {
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchUnchecked(
|
||||
GenericMessage.class,
|
||||
(GenericMessage<String> msg) -> {
|
||||
GenericMessage<String> message = msg;
|
||||
getSender().tell(message.value.toUpperCase(), getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
static class GenericActorWithPredicate extends AbstractActor {
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
FI.TypedPredicate<GenericMessage<String>> typedPredicate = s -> !s.value.isEmpty();
|
||||
|
||||
return receiveBuilder()
|
||||
.matchUnchecked(
|
||||
GenericMessage.class,
|
||||
typedPredicate,
|
||||
(GenericMessage<String> msg) -> {
|
||||
getSender().tell(msg.value.toUpperCase(), getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
static class GenericActorWithPredicateAlwaysResponse extends AbstractActor {
|
||||
private boolean alwaysResponse() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchUnchecked(
|
||||
GenericMessage.class,
|
||||
this::alwaysResponse,
|
||||
(GenericMessage<String> msg) -> {
|
||||
getSender().tell(msg.value.toUpperCase(), getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIt() {
|
||||
|
||||
new TestKit(system) {
|
||||
{
|
||||
ActorRef testactor = system.actorOf(Props.create(MessageInitExample.class), "testactor");
|
||||
String msg = "U OK?";
|
||||
|
||||
testactor.tell(msg, getRef());
|
||||
expectNoMessage(Duration.ofSeconds(1));
|
||||
|
||||
testactor.tell("init", getRef());
|
||||
testactor.tell(msg, getRef());
|
||||
expectMsgEquals("Up and running");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenericActor() {
|
||||
new TestKit(system) {
|
||||
{
|
||||
ActorRef genericTestActor =
|
||||
system.actorOf(Props.create(GenericActor.class), "genericActor");
|
||||
GenericMessage<String> genericMessage = new GenericMessage<String>("a");
|
||||
|
||||
genericTestActor.tell(genericMessage, getRef());
|
||||
expectMsgEquals("A");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actorShouldNotRespondForEmptyMessage() {
|
||||
new TestKit(system) {
|
||||
{
|
||||
ActorRef genericTestActor =
|
||||
system.actorOf(
|
||||
Props.create(GenericActorWithPredicate.class), "genericActorWithPredicate");
|
||||
GenericMessage<String> emptyGenericMessage = new GenericMessage<String>("");
|
||||
GenericMessage<String> nonEmptyGenericMessage = new GenericMessage<String>("a");
|
||||
|
||||
genericTestActor.tell(emptyGenericMessage, getRef());
|
||||
expectNoMessage();
|
||||
|
||||
genericTestActor.tell(nonEmptyGenericMessage, getRef());
|
||||
expectMsgEquals("A");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actorShouldAlwaysRespondForEmptyMessage() {
|
||||
new TestKit(system) {
|
||||
{
|
||||
ActorRef genericTestActor =
|
||||
system.actorOf(
|
||||
Props.create(GenericActorWithPredicateAlwaysResponse.class),
|
||||
"genericActorWithPredicateAlwaysResponse");
|
||||
GenericMessage<String> emptyGenericMessage = new GenericMessage<String>("");
|
||||
GenericMessage<String> nonEmptyGenericMessage = new GenericMessage<String>("a");
|
||||
|
||||
genericTestActor.tell(emptyGenericMessage, getRef());
|
||||
expectMsg("");
|
||||
|
||||
genericTestActor.tell(nonEmptyGenericMessage, getRef());
|
||||
expectMsgEquals("A");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
138
docs/src/test/java/jdocs/actor/Messages.java
Normal file
138
docs/src/test/java/jdocs/actor/Messages.java
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class Messages {
|
||||
public
|
||||
// #immutable-message
|
||||
static class ImmutableMessage {
|
||||
private final int sequenceNumber;
|
||||
private final List<String> values;
|
||||
|
||||
public ImmutableMessage(int sequenceNumber, List<String> values) {
|
||||
this.sequenceNumber = sequenceNumber;
|
||||
this.values = Collections.unmodifiableList(new ArrayList<String>(values));
|
||||
}
|
||||
|
||||
public int getSequenceNumber() {
|
||||
return sequenceNumber;
|
||||
}
|
||||
|
||||
public List<String> getValues() {
|
||||
return values;
|
||||
}
|
||||
}
|
||||
// #immutable-message
|
||||
|
||||
public static class DoIt {
|
||||
private final ImmutableMessage msg;
|
||||
|
||||
DoIt(ImmutableMessage msg) {
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public ImmutableMessage getMsg() {
|
||||
return msg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
DoIt doIt = (DoIt) o;
|
||||
|
||||
if (!msg.equals(doIt.msg)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return msg.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DoIt{" + "msg=" + msg + '}';
|
||||
}
|
||||
}
|
||||
|
||||
public static class Message {
|
||||
final String str;
|
||||
|
||||
Message(String str) {
|
||||
this.str = str;
|
||||
}
|
||||
|
||||
public String getStr() {
|
||||
return str;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Message message = (Message) o;
|
||||
|
||||
if (!str.equals(message.str)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return str.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Message{" + "str='" + str + '\'' + '}';
|
||||
}
|
||||
}
|
||||
|
||||
public enum Swap {
|
||||
Swap
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
final String x;
|
||||
final String s;
|
||||
|
||||
public Result(String x, String s) {
|
||||
this.x = x;
|
||||
this.s = s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((s == null) ? 0 : s.hashCode());
|
||||
result = prime * result + ((x == null) ? 0 : x.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
Result other = (Result) obj;
|
||||
if (s == null) {
|
||||
if (other.s != null) return false;
|
||||
} else if (!s.equals(other.s)) return false;
|
||||
if (x == null) {
|
||||
if (other.x != null) return false;
|
||||
} else if (!x.equals(other.x)) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
docs/src/test/java/jdocs/actor/MyActor.java
Normal file
35
docs/src/test/java/jdocs/actor/MyActor.java
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
// #imports
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.event.Logging;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
|
||||
// #imports
|
||||
|
||||
// #my-actor
|
||||
public class MyActor extends AbstractActor {
|
||||
private final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
s -> {
|
||||
log.info("Received String message: {}", s);
|
||||
// #my-actor
|
||||
// #reply
|
||||
getSender().tell(s, getSelf());
|
||||
// #reply
|
||||
// #my-actor
|
||||
})
|
||||
.matchAny(o -> log.info("received unknown message"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #my-actor
|
||||
13
docs/src/test/java/jdocs/actor/MyBoundedActor.java
Normal file
13
docs/src/test/java/jdocs/actor/MyBoundedActor.java
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
// #my-bounded-classic-actor
|
||||
import org.apache.pekko.dispatch.BoundedMessageQueueSemantics;
|
||||
import org.apache.pekko.dispatch.RequiresMessageQueue;
|
||||
|
||||
public class MyBoundedActor extends MyActor
|
||||
implements RequiresMessageQueue<BoundedMessageQueueSemantics> {}
|
||||
// #my-bounded-classic-actor
|
||||
25
docs/src/test/java/jdocs/actor/MyStoppingActor.java
Normal file
25
docs/src/test/java/jdocs/actor/MyStoppingActor.java
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
// #my-stopping-actor
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
|
||||
public class MyStoppingActor extends AbstractActor {
|
||||
|
||||
ActorRef child = null;
|
||||
|
||||
// ... creation of child ...
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchEquals("interrupt-child", m -> getContext().stop(child))
|
||||
.matchEquals("done", m -> getContext().stop(getSelf()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #my-stopping-actor
|
||||
46
docs/src/test/java/jdocs/actor/SampleActor.java
Normal file
46
docs/src/test/java/jdocs/actor/SampleActor.java
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
// #sample-actor
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
|
||||
public class SampleActor extends AbstractActor {
|
||||
|
||||
private Receive guarded =
|
||||
receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
s -> s.contains("guard"),
|
||||
s -> {
|
||||
getSender().tell("contains(guard): " + s, getSelf());
|
||||
getContext().unbecome();
|
||||
})
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Double.class,
|
||||
d -> {
|
||||
getSender().tell(d.isNaN() ? 0 : d, getSelf());
|
||||
})
|
||||
.match(
|
||||
Integer.class,
|
||||
i -> {
|
||||
getSender().tell(i * 10, getSelf());
|
||||
})
|
||||
.match(
|
||||
String.class,
|
||||
s -> s.startsWith("guard"),
|
||||
s -> {
|
||||
getSender().tell("startsWith(guard): " + s.toUpperCase(), getSelf());
|
||||
getContext().become(guarded, false);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #sample-actor
|
||||
57
docs/src/test/java/jdocs/actor/SampleActorTest.java
Normal file
57
docs/src/test/java/jdocs/actor/SampleActorTest.java
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class SampleActorTest extends AbstractJavaTest {
|
||||
|
||||
static ActorSystem system;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
system = ActorSystem.create("SampleActorTest");
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDown() {
|
||||
TestKit.shutdownActorSystem(system);
|
||||
system = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSampleActor() {
|
||||
new TestKit(system) {
|
||||
{
|
||||
final ActorRef subject = system.actorOf(Props.create(SampleActor.class), "sample-actor");
|
||||
final ActorRef probeRef = getRef();
|
||||
|
||||
subject.tell(47.11, probeRef);
|
||||
subject.tell("and no guard in the beginning", probeRef);
|
||||
subject.tell("guard is a good thing", probeRef);
|
||||
subject.tell(47.11, probeRef);
|
||||
subject.tell(4711, probeRef);
|
||||
subject.tell("and no guard in the beginning", probeRef);
|
||||
subject.tell(4711, probeRef);
|
||||
subject.tell("and an unmatched message", probeRef);
|
||||
|
||||
expectMsgEquals(47.11);
|
||||
assertTrue(expectMsgClass(String.class).startsWith("startsWith(guard):"));
|
||||
assertTrue(expectMsgClass(String.class).startsWith("contains(guard):"));
|
||||
expectMsgEquals(47110);
|
||||
expectNoMessage();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
93
docs/src/test/java/jdocs/actor/SchedulerDocTest.java
Normal file
93
docs/src/test/java/jdocs/actor/SchedulerDocTest.java
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
// #imports1
|
||||
import java.time.Duration;
|
||||
// #imports1
|
||||
|
||||
// #imports2
|
||||
import org.apache.pekko.actor.Cancellable;
|
||||
// #imports2
|
||||
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.testkit.AkkaSpec;
|
||||
import org.apache.pekko.testkit.AkkaJUnitActorSystemResource;
|
||||
import org.junit.*;
|
||||
|
||||
public class SchedulerDocTest extends AbstractJavaTest {
|
||||
|
||||
@ClassRule
|
||||
public static AkkaJUnitActorSystemResource actorSystemResource =
|
||||
new AkkaJUnitActorSystemResource("SchedulerDocTest", AkkaSpec.testConf());
|
||||
|
||||
private final ActorSystem system = actorSystemResource.getSystem();
|
||||
private ActorRef testActor = system.actorOf(Props.create(MyActor.class));
|
||||
|
||||
@Test
|
||||
public void scheduleOneOffTask() {
|
||||
// #schedule-one-off-message
|
||||
system
|
||||
.scheduler()
|
||||
.scheduleOnce(
|
||||
Duration.ofMillis(50), testActor, "foo", system.dispatcher(), ActorRef.noSender());
|
||||
// #schedule-one-off-message
|
||||
|
||||
// #schedule-one-off-thunk
|
||||
system
|
||||
.scheduler()
|
||||
.scheduleOnce(
|
||||
Duration.ofMillis(50),
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
testActor.tell(System.currentTimeMillis(), ActorRef.noSender());
|
||||
}
|
||||
},
|
||||
system.dispatcher());
|
||||
// #schedule-one-off-thunk
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scheduleRecurringTask() {
|
||||
// #schedule-recurring
|
||||
class Ticker extends AbstractActor {
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchEquals(
|
||||
"Tick",
|
||||
m -> {
|
||||
// Do something
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
ActorRef tickActor = system.actorOf(Props.create(Ticker.class, this));
|
||||
|
||||
// This will schedule to send the Tick-message
|
||||
// to the tickActor after 0ms repeating every 50ms
|
||||
Cancellable cancellable =
|
||||
system
|
||||
.scheduler()
|
||||
.scheduleWithFixedDelay(
|
||||
Duration.ZERO,
|
||||
Duration.ofMillis(50),
|
||||
tickActor,
|
||||
"Tick",
|
||||
system.dispatcher(),
|
||||
ActorRef.noSender());
|
||||
|
||||
// This cancels further Ticks to be sent
|
||||
cancellable.cancel();
|
||||
// #schedule-recurring
|
||||
system.stop(tickActor);
|
||||
}
|
||||
}
|
||||
47
docs/src/test/java/jdocs/actor/TimerDocTest.java
Normal file
47
docs/src/test/java/jdocs/actor/TimerDocTest.java
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (C) 2017-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor;
|
||||
|
||||
// #timers
|
||||
import java.time.Duration;
|
||||
import org.apache.pekko.actor.AbstractActorWithTimers;
|
||||
|
||||
// #timers
|
||||
|
||||
public class TimerDocTest {
|
||||
|
||||
public
|
||||
// #timers
|
||||
static class MyActor extends AbstractActorWithTimers {
|
||||
|
||||
private static Object TICK_KEY = "TickKey";
|
||||
|
||||
private static final class FirstTick {}
|
||||
|
||||
private static final class Tick {}
|
||||
|
||||
public MyActor() {
|
||||
getTimers().startSingleTimer(TICK_KEY, new FirstTick(), Duration.ofMillis(500));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
FirstTick.class,
|
||||
message -> {
|
||||
// do something useful here
|
||||
getTimers().startTimerWithFixedDelay(TICK_KEY, new Tick(), Duration.ofSeconds(1));
|
||||
})
|
||||
.match(
|
||||
Tick.class,
|
||||
message -> {
|
||||
// do something useful here
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #timers
|
||||
}
|
||||
158
docs/src/test/java/jdocs/actor/fsm/Buncher.java
Normal file
158
docs/src/test/java/jdocs/actor/fsm/Buncher.java
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor.fsm;
|
||||
|
||||
// #simple-imports
|
||||
import org.apache.pekko.actor.AbstractFSM;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.japi.pf.UnitMatch;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.time.Duration;
|
||||
// #simple-imports
|
||||
|
||||
import static jdocs.actor.fsm.Buncher.Data;
|
||||
import static jdocs.actor.fsm.Buncher.State.*;
|
||||
import static jdocs.actor.fsm.Buncher.State;
|
||||
import static jdocs.actor.fsm.Buncher.Uninitialized.*;
|
||||
import static jdocs.actor.fsm.Events.*;
|
||||
|
||||
// #simple-fsm
|
||||
public class Buncher extends AbstractFSM<State, Data> {
|
||||
{
|
||||
// #fsm-body
|
||||
startWith(Idle, Uninitialized);
|
||||
|
||||
// #when-syntax
|
||||
when(
|
||||
Idle,
|
||||
matchEvent(
|
||||
SetTarget.class,
|
||||
Uninitialized.class,
|
||||
(setTarget, uninitialized) ->
|
||||
stay().using(new Todo(setTarget.getRef(), new LinkedList<>()))));
|
||||
// #when-syntax
|
||||
|
||||
// #transition-elided
|
||||
onTransition(
|
||||
matchState(
|
||||
Active,
|
||||
Idle,
|
||||
() -> {
|
||||
// reuse this matcher
|
||||
final UnitMatch<Data> m =
|
||||
UnitMatch.create(
|
||||
matchData(
|
||||
Todo.class,
|
||||
todo ->
|
||||
todo.getTarget().tell(new Batch(todo.getQueue()), getSelf())));
|
||||
m.match(stateData());
|
||||
})
|
||||
.state(
|
||||
Idle,
|
||||
Active,
|
||||
() -> {
|
||||
/* Do something here */
|
||||
}));
|
||||
// #transition-elided
|
||||
|
||||
when(
|
||||
Active,
|
||||
Duration.ofSeconds(1L),
|
||||
matchEvent(
|
||||
Arrays.asList(Flush.class, StateTimeout()),
|
||||
Todo.class,
|
||||
(event, todo) -> goTo(Idle).using(todo.copy(new LinkedList<>()))));
|
||||
|
||||
// #unhandled-elided
|
||||
whenUnhandled(
|
||||
matchEvent(
|
||||
Queue.class,
|
||||
Todo.class,
|
||||
(queue, todo) -> goTo(Active).using(todo.addElement(queue.getObj())))
|
||||
.anyEvent(
|
||||
(event, state) -> {
|
||||
log()
|
||||
.warning(
|
||||
"received unhandled request {} in state {}/{}",
|
||||
event,
|
||||
stateName(),
|
||||
state);
|
||||
return stay();
|
||||
}));
|
||||
// #unhandled-elided
|
||||
|
||||
initialize();
|
||||
// #fsm-body
|
||||
}
|
||||
// #simple-fsm
|
||||
|
||||
static
|
||||
// #simple-state
|
||||
// states
|
||||
enum State {
|
||||
Idle,
|
||||
Active
|
||||
}
|
||||
|
||||
// #simple-state
|
||||
static
|
||||
// #simple-state
|
||||
// state data
|
||||
interface Data {}
|
||||
|
||||
// #simple-state
|
||||
static
|
||||
// #simple-state
|
||||
enum Uninitialized implements Data {
|
||||
Uninitialized
|
||||
}
|
||||
|
||||
// #simple-state
|
||||
static
|
||||
// #simple-state
|
||||
final class Todo implements Data {
|
||||
private final ActorRef target;
|
||||
private final List<Object> queue;
|
||||
|
||||
public Todo(ActorRef target, List<Object> queue) {
|
||||
this.target = target;
|
||||
this.queue = queue;
|
||||
}
|
||||
|
||||
public ActorRef getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
public List<Object> getQueue() {
|
||||
return queue;
|
||||
}
|
||||
// #boilerplate
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Todo{" + "target=" + target + ", queue=" + queue + '}';
|
||||
}
|
||||
|
||||
public Todo addElement(Object element) {
|
||||
List<Object> nQueue = new LinkedList<>(queue);
|
||||
nQueue.add(element);
|
||||
return new Todo(this.target, nQueue);
|
||||
}
|
||||
|
||||
public Todo copy(List<Object> queue) {
|
||||
return new Todo(this.target, queue);
|
||||
}
|
||||
|
||||
public Todo copy(ActorRef target) {
|
||||
return new Todo(target, this.queue);
|
||||
}
|
||||
// #boilerplate
|
||||
}
|
||||
// #simple-state
|
||||
// #simple-fsm
|
||||
}
|
||||
// #simple-fsm
|
||||
80
docs/src/test/java/jdocs/actor/fsm/BuncherTest.java
Normal file
80
docs/src/test/java/jdocs/actor/fsm/BuncherTest.java
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor.fsm;
|
||||
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import static jdocs.actor.fsm.Events.Batch;
|
||||
import static jdocs.actor.fsm.Events.Queue;
|
||||
import static jdocs.actor.fsm.Events.SetTarget;
|
||||
import static jdocs.actor.fsm.Events.Flush.Flush;
|
||||
|
||||
// #test-code
|
||||
public class BuncherTest extends AbstractJavaTest {
|
||||
|
||||
static ActorSystem system;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
system = ActorSystem.create("BuncherTest");
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDown() {
|
||||
TestKit.shutdownActorSystem(system);
|
||||
system = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuncherActorBatchesCorrectly() {
|
||||
new TestKit(system) {
|
||||
{
|
||||
final ActorRef buncher = system.actorOf(Props.create(Buncher.class));
|
||||
final ActorRef probe = getRef();
|
||||
|
||||
buncher.tell(new SetTarget(probe), probe);
|
||||
buncher.tell(new Queue(42), probe);
|
||||
buncher.tell(new Queue(43), probe);
|
||||
LinkedList<Object> list1 = new LinkedList<>();
|
||||
list1.add(42);
|
||||
list1.add(43);
|
||||
expectMsgEquals(new Batch(list1));
|
||||
buncher.tell(new Queue(44), probe);
|
||||
buncher.tell(Flush, probe);
|
||||
buncher.tell(new Queue(45), probe);
|
||||
LinkedList<Object> list2 = new LinkedList<>();
|
||||
list2.add(44);
|
||||
expectMsgEquals(new Batch(list2));
|
||||
LinkedList<Object> list3 = new LinkedList<>();
|
||||
list3.add(45);
|
||||
expectMsgEquals(new Batch(list3));
|
||||
system.stop(buncher);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuncherActorDoesntBatchUninitialized() {
|
||||
new TestKit(system) {
|
||||
{
|
||||
final ActorRef buncher = system.actorOf(Props.create(Buncher.class));
|
||||
final ActorRef probe = getRef();
|
||||
|
||||
buncher.tell(new Queue(42), probe);
|
||||
expectNoMessage();
|
||||
system.stop(buncher);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
// #test-code
|
||||
109
docs/src/test/java/jdocs/actor/fsm/Events.java
Normal file
109
docs/src/test/java/jdocs/actor/fsm/Events.java
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor.fsm;
|
||||
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import java.util.List;
|
||||
|
||||
public class Events {
|
||||
|
||||
public
|
||||
// #simple-events
|
||||
static final class SetTarget {
|
||||
private final ActorRef ref;
|
||||
|
||||
public SetTarget(ActorRef ref) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
public ActorRef getRef() {
|
||||
return ref;
|
||||
}
|
||||
// #boilerplate
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SetTarget{" + "ref=" + ref + '}';
|
||||
}
|
||||
// #boilerplate
|
||||
}
|
||||
|
||||
// #simple-events
|
||||
public
|
||||
// #simple-events
|
||||
static final class Queue {
|
||||
private final Object obj;
|
||||
|
||||
public Queue(Object obj) {
|
||||
this.obj = obj;
|
||||
}
|
||||
|
||||
public Object getObj() {
|
||||
return obj;
|
||||
}
|
||||
// #boilerplate
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Queue{" + "obj=" + obj + '}';
|
||||
}
|
||||
// #boilerplate
|
||||
}
|
||||
|
||||
// #simple-events
|
||||
public
|
||||
// #simple-events
|
||||
static final class Batch {
|
||||
private final List<Object> list;
|
||||
|
||||
public Batch(List<Object> list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
public List<Object> getList() {
|
||||
return list;
|
||||
}
|
||||
// #boilerplate
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Batch batch = (Batch) o;
|
||||
|
||||
return list.equals(batch.list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return list.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
builder.append("Batch{list=");
|
||||
list.stream()
|
||||
.forEachOrdered(
|
||||
e -> {
|
||||
builder.append(e);
|
||||
builder.append(",");
|
||||
});
|
||||
int len = builder.length();
|
||||
builder.replace(len, len, "}");
|
||||
return builder.toString();
|
||||
}
|
||||
// #boilerplate
|
||||
}
|
||||
|
||||
// #simple-events
|
||||
public
|
||||
// #simple-events
|
||||
static enum Flush {
|
||||
Flush
|
||||
}
|
||||
// #simple-events
|
||||
}
|
||||
222
docs/src/test/java/jdocs/actor/fsm/FSMDocTest.java
Normal file
222
docs/src/test/java/jdocs/actor/fsm/FSMDocTest.java
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor.fsm;
|
||||
|
||||
import org.apache.pekko.actor.*;
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import static jdocs.actor.fsm.FSMDocTest.StateType.*;
|
||||
import static jdocs.actor.fsm.FSMDocTest.Messages.*;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class FSMDocTest extends AbstractJavaTest {
|
||||
static ActorSystem system;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
system = ActorSystem.create("FSMDocTest");
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDown() {
|
||||
TestKit.shutdownActorSystem(system);
|
||||
system = null;
|
||||
}
|
||||
|
||||
public enum StateType {
|
||||
SomeState,
|
||||
Processing,
|
||||
Idle,
|
||||
Active,
|
||||
Error
|
||||
}
|
||||
|
||||
public enum Messages {
|
||||
WillDo,
|
||||
Tick
|
||||
}
|
||||
|
||||
public enum Data {
|
||||
Foo,
|
||||
Bar
|
||||
};
|
||||
|
||||
public static interface X {};
|
||||
|
||||
public static class DummyFSM extends AbstractFSM<StateType, Integer> {
|
||||
Integer newData = 42;
|
||||
// #alt-transition-syntax
|
||||
public void handler(StateType from, StateType to) {
|
||||
// handle transition here
|
||||
}
|
||||
|
||||
// #alt-transition-syntax
|
||||
{
|
||||
// #modifier-syntax
|
||||
when(
|
||||
SomeState,
|
||||
matchAnyEvent(
|
||||
(msg, data) -> {
|
||||
return goTo(Processing)
|
||||
.using(newData)
|
||||
.forMax(Duration.ofSeconds(5))
|
||||
.replying(WillDo);
|
||||
}));
|
||||
// #modifier-syntax
|
||||
|
||||
// #NullFunction
|
||||
when(SomeState, AbstractFSM.NullFunction());
|
||||
// #NullFunction
|
||||
|
||||
// #transition-syntax
|
||||
onTransition(
|
||||
matchState(
|
||||
Idle,
|
||||
Active,
|
||||
() -> startTimerWithFixedDelay("timeout", Tick, Duration.ofSeconds(1L)))
|
||||
.state(Active, null, () -> cancelTimer("timeout"))
|
||||
.state(null, Idle, (f, t) -> log().info("entering Idle from " + f)));
|
||||
// #transition-syntax
|
||||
|
||||
// #alt-transition-syntax
|
||||
onTransition(this::handler);
|
||||
// #alt-transition-syntax
|
||||
|
||||
// #stop-syntax
|
||||
when(
|
||||
Error,
|
||||
matchEventEquals(
|
||||
"stop",
|
||||
(event, data) -> {
|
||||
// do cleanup ...
|
||||
return stop();
|
||||
}));
|
||||
// #stop-syntax
|
||||
|
||||
// #termination-syntax
|
||||
onTermination(
|
||||
matchStop(
|
||||
Normal(),
|
||||
(state, data) -> {
|
||||
/* Do something here */
|
||||
})
|
||||
.stop(
|
||||
Shutdown(),
|
||||
(state, data) -> {
|
||||
/* Do something here */
|
||||
})
|
||||
.stop(
|
||||
Failure.class,
|
||||
(reason, state, data) -> {
|
||||
/* Do something here */
|
||||
}));
|
||||
// #termination-syntax
|
||||
|
||||
// #unhandled-syntax
|
||||
whenUnhandled(
|
||||
matchEvent(
|
||||
X.class,
|
||||
(x, data) -> {
|
||||
log().info("Received unhandled event: " + x);
|
||||
return stay();
|
||||
})
|
||||
.anyEvent(
|
||||
(event, data) -> {
|
||||
log().warning("Received unknown event: " + event);
|
||||
return goTo(Error);
|
||||
}));
|
||||
}
|
||||
// #unhandled-syntax
|
||||
}
|
||||
|
||||
public
|
||||
// #logging-fsm
|
||||
static class MyFSM extends AbstractLoggingFSM<StateType, Data> {
|
||||
// #body-elided
|
||||
// #logging-fsm
|
||||
ActorRef target = null;
|
||||
// #logging-fsm
|
||||
@Override
|
||||
public int logDepth() {
|
||||
return 12;
|
||||
}
|
||||
|
||||
{
|
||||
onTermination(
|
||||
matchStop(
|
||||
Failure.class,
|
||||
(reason, state, data) -> {
|
||||
String lastEvents = getLog().mkString("\n\t");
|
||||
log()
|
||||
.warning(
|
||||
"Failure in state "
|
||||
+ state
|
||||
+ " with data "
|
||||
+ data
|
||||
+ "\n"
|
||||
+ "Events leading up to this point:\n\t"
|
||||
+ lastEvents);
|
||||
// #logging-fsm
|
||||
target.tell(reason.cause(), getSelf());
|
||||
target.tell(state, getSelf());
|
||||
target.tell(data, getSelf());
|
||||
target.tell(lastEvents, getSelf());
|
||||
// #logging-fsm
|
||||
}));
|
||||
// ...
|
||||
// #logging-fsm
|
||||
startWith(SomeState, Data.Foo);
|
||||
when(
|
||||
SomeState,
|
||||
matchEvent(
|
||||
ActorRef.class,
|
||||
Data.class,
|
||||
(ref, data) -> {
|
||||
target = ref;
|
||||
target.tell("going active", getSelf());
|
||||
return goTo(Active);
|
||||
}));
|
||||
when(
|
||||
Active,
|
||||
matchEventEquals(
|
||||
"stop",
|
||||
(event, data) -> {
|
||||
target.tell("stopping", getSelf());
|
||||
return stop(new Failure("This is not the error you're looking for"));
|
||||
}));
|
||||
initialize();
|
||||
// #logging-fsm
|
||||
}
|
||||
// #body-elided
|
||||
}
|
||||
// #logging-fsm
|
||||
|
||||
@Test
|
||||
public void testLoggingFSM() {
|
||||
new TestKit(system) {
|
||||
{
|
||||
final ActorRef logger = system.actorOf(Props.create(MyFSM.class));
|
||||
final ActorRef probe = getRef();
|
||||
|
||||
logger.tell(probe, probe);
|
||||
expectMsgEquals("going active");
|
||||
logger.tell("stop", probe);
|
||||
expectMsgEquals("stopping");
|
||||
expectMsgEquals("This is not the error you're looking for");
|
||||
expectMsgEquals(Active);
|
||||
expectMsgEquals(Data.Foo);
|
||||
String msg = expectMsgClass(String.class);
|
||||
assertTrue(msg.startsWith("LogEntry(SomeState,Foo,Actor[akka://FSMDocTest/system/"));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor.io.dns;
|
||||
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.io.Dns;
|
||||
import org.apache.pekko.io.dns.DnsProtocol;
|
||||
|
||||
import static org.apache.pekko.pattern.Patterns.ask;
|
||||
|
||||
import scala.Option;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
public class DnsCompileOnlyDocTest {
|
||||
public static void example() {
|
||||
ActorSystem system = ActorSystem.create();
|
||||
|
||||
ActorRef actorRef = null;
|
||||
final Duration timeout = Duration.ofMillis(1000L);
|
||||
|
||||
// #resolve
|
||||
Option<DnsProtocol.Resolved> initial =
|
||||
Dns.get(system)
|
||||
.cache()
|
||||
.resolve(
|
||||
new DnsProtocol.Resolve("google.com", DnsProtocol.ipRequestType()),
|
||||
system,
|
||||
actorRef);
|
||||
Option<DnsProtocol.Resolved> cached =
|
||||
Dns.get(system)
|
||||
.cache()
|
||||
.cached(new DnsProtocol.Resolve("google.com", DnsProtocol.ipRequestType()));
|
||||
// #resolve
|
||||
|
||||
{
|
||||
// #actor-api-inet-address
|
||||
final ActorRef dnsManager = Dns.get(system).manager();
|
||||
CompletionStage<Object> resolved =
|
||||
ask(
|
||||
dnsManager,
|
||||
new DnsProtocol.Resolve("google.com", DnsProtocol.ipRequestType()),
|
||||
timeout);
|
||||
// #actor-api-inet-address
|
||||
|
||||
}
|
||||
|
||||
{
|
||||
// #actor-api-async
|
||||
final ActorRef dnsManager = Dns.get(system).manager();
|
||||
CompletionStage<Object> resolved =
|
||||
ask(dnsManager, DnsProtocol.resolve("google.com"), timeout);
|
||||
// #actor-api-async
|
||||
}
|
||||
|
||||
{
|
||||
// #srv
|
||||
final ActorRef dnsManager = Dns.get(system).manager();
|
||||
CompletionStage<Object> resolved =
|
||||
ask(dnsManager, DnsProtocol.resolve("google.com", DnsProtocol.srvRequestType()), timeout);
|
||||
// #srv
|
||||
}
|
||||
}
|
||||
}
|
||||
37
docs/src/test/java/jdocs/actor/typed/BlockingActor.java
Normal file
37
docs/src/test/java/jdocs/actor/typed/BlockingActor.java
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (C) 2019-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor.typed;
|
||||
|
||||
// #blocking-in-actor
|
||||
import org.apache.pekko.actor.typed.*;
|
||||
import org.apache.pekko.actor.typed.javadsl.*;
|
||||
|
||||
public class BlockingActor extends AbstractBehavior<Integer> {
|
||||
public static Behavior<Integer> create() {
|
||||
return Behaviors.setup(BlockingActor::new);
|
||||
}
|
||||
|
||||
private BlockingActor(ActorContext<Integer> context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<Integer> createReceive() {
|
||||
return newReceiveBuilder()
|
||||
.onMessage(
|
||||
Integer.class,
|
||||
i -> {
|
||||
// DO NOT DO THIS HERE: this is an example of incorrect code,
|
||||
// better alternatives are described further on.
|
||||
|
||||
// block for 5 seconds, representing blocking I/O, etc
|
||||
Thread.sleep(5000);
|
||||
System.out.println("Blocking operation finished: " + i);
|
||||
return Behaviors.same();
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #blocking-in-actor
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor.typed;
|
||||
|
||||
import org.apache.pekko.actor.typed.*;
|
||||
import org.apache.pekko.actor.typed.javadsl.*;
|
||||
|
||||
public class BlockingDispatcherTest {
|
||||
public static void main(String args[]) {
|
||||
// #blocking-main
|
||||
Behavior<Void> root =
|
||||
Behaviors.setup(
|
||||
context -> {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
context.spawn(BlockingActor.create(), "BlockingActor-" + i).tell(i);
|
||||
context.spawn(PrintActor.create(), "PrintActor-" + i).tell(i);
|
||||
}
|
||||
return Behaviors.ignore();
|
||||
});
|
||||
// #blocking-main
|
||||
|
||||
ActorSystem<Void> system = ActorSystem.<Void>create(root, "BlockingDispatcherTest");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor.typed;
|
||||
|
||||
import org.apache.pekko.Done;
|
||||
import org.apache.pekko.actor.Cancellable;
|
||||
import org.apache.pekko.actor.CoordinatedShutdown;
|
||||
import org.apache.pekko.actor.typed.ActorRef;
|
||||
import org.apache.pekko.actor.typed.ActorSystem;
|
||||
import org.apache.pekko.actor.typed.Behavior;
|
||||
import org.apache.pekko.actor.typed.javadsl.*;
|
||||
// #coordinated-shutdown-addTask
|
||||
import static org.apache.pekko.actor.typed.javadsl.AskPattern.ask;
|
||||
|
||||
// #coordinated-shutdown-addTask
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
public class CoordinatedActorShutdownTest {
|
||||
|
||||
// #coordinated-shutdown-addTask
|
||||
public static class MyActor extends AbstractBehavior<MyActor.Messages> {
|
||||
interface Messages {}
|
||||
|
||||
// ...
|
||||
|
||||
static final class Stop implements Messages {
|
||||
final ActorRef<Done> replyTo;
|
||||
|
||||
Stop(ActorRef<Done> replyTo) {
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
// #coordinated-shutdown-addTask
|
||||
|
||||
public static Behavior<Messages> create() {
|
||||
return Behaviors.setup(MyActor::new);
|
||||
}
|
||||
|
||||
private MyActor(ActorContext<Messages> context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
// #coordinated-shutdown-addTask
|
||||
@Override
|
||||
public Receive<Messages> createReceive() {
|
||||
return newReceiveBuilder().onMessage(Stop.class, this::stop).build();
|
||||
}
|
||||
|
||||
private Behavior<Messages> stop(Stop stop) {
|
||||
// shut down the actor internal
|
||||
// ...
|
||||
stop.replyTo.tell(Done.done());
|
||||
return Behaviors.stopped();
|
||||
}
|
||||
}
|
||||
|
||||
// #coordinated-shutdown-addTask
|
||||
|
||||
public static class Root extends AbstractBehavior<Void> {
|
||||
|
||||
public static Behavior<Void> create() {
|
||||
return Behaviors.setup(
|
||||
context -> {
|
||||
ActorRef<MyActor.Messages> myActor = context.spawn(MyActor.create(), "my-actor");
|
||||
ActorSystem<Void> system = context.getSystem();
|
||||
// #coordinated-shutdown-addTask
|
||||
CoordinatedShutdown.get(system)
|
||||
.addTask(
|
||||
CoordinatedShutdown.PhaseBeforeServiceUnbind(),
|
||||
"someTaskName",
|
||||
() ->
|
||||
ask(myActor, MyActor.Stop::new, Duration.ofSeconds(5), system.scheduler()));
|
||||
// #coordinated-shutdown-addTask
|
||||
return Behaviors.empty();
|
||||
});
|
||||
}
|
||||
|
||||
private Root(ActorContext<Void> context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<Void> createReceive() {
|
||||
return newReceiveBuilder().build();
|
||||
}
|
||||
}
|
||||
|
||||
private CompletionStage<Done> cleanup() {
|
||||
return CompletableFuture.completedFuture(Done.done());
|
||||
}
|
||||
|
||||
public void mount() {
|
||||
ActorSystem<Void> system = ActorSystem.create(Root.create(), "main");
|
||||
|
||||
// #coordinated-shutdown-cancellable
|
||||
Cancellable cancellable =
|
||||
CoordinatedShutdown.get(system)
|
||||
.addCancellableTask(
|
||||
CoordinatedShutdown.PhaseBeforeServiceUnbind(), "someTaskCleanup", () -> cleanup());
|
||||
// much later...
|
||||
cancellable.cancel();
|
||||
// #coordinated-shutdown-cancellable
|
||||
|
||||
// #coordinated-shutdown-jvm-hook
|
||||
CoordinatedShutdown.get(system)
|
||||
.addJvmShutdownHook(() -> System.out.println("custom JVM shutdown hook..."));
|
||||
// #coordinated-shutdown-jvm-hook
|
||||
|
||||
// don't run this
|
||||
if (false) {
|
||||
// #coordinated-shutdown-run
|
||||
// shut down with `ActorSystemTerminateReason`
|
||||
system.terminate();
|
||||
|
||||
// or define a specific reason
|
||||
class UserInitiatedShutdown implements CoordinatedShutdown.Reason {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UserInitiatedShutdown";
|
||||
}
|
||||
}
|
||||
|
||||
CompletionStage<Done> done =
|
||||
CoordinatedShutdown.get(system).runAll(new UserInitiatedShutdown());
|
||||
// #coordinated-shutdown-run
|
||||
}
|
||||
}
|
||||
}
|
||||
55
docs/src/test/java/jdocs/actor/typed/DispatcherDocTest.java
Normal file
55
docs/src/test/java/jdocs/actor/typed/DispatcherDocTest.java
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor.typed;
|
||||
|
||||
import scala.concurrent.ExecutionContextExecutor;
|
||||
|
||||
import org.apache.pekko.actor.typed.*;
|
||||
import org.apache.pekko.actor.typed.javadsl.*;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DispatcherDocTest {
|
||||
|
||||
private final ActorSystem<Void> system = null;
|
||||
private final ActorContext<Void> context = null;
|
||||
|
||||
public void defineDispatcherInCode() {
|
||||
// #defining-dispatcher-in-code
|
||||
ActorRef<Integer> myActor =
|
||||
context.spawn(
|
||||
PrintActor.create(), "PrintActor", DispatcherSelector.fromConfig("my-dispatcher"));
|
||||
// #defining-dispatcher-in-code
|
||||
}
|
||||
|
||||
public void defineFixedPoolSizeDispatcher() {
|
||||
// #defining-fixed-pool-size-dispatcher
|
||||
ActorRef<Integer> myActor =
|
||||
context.spawn(
|
||||
PrintActor.create(),
|
||||
"PrintActor",
|
||||
DispatcherSelector.fromConfig("blocking-io-dispatcher"));
|
||||
// #defining-fixed-pool-size-dispatcher
|
||||
}
|
||||
|
||||
public void definePinnedDispatcher() {
|
||||
// #defining-pinned-dispatcher
|
||||
ActorRef<Integer> myActor =
|
||||
context.spawn(
|
||||
PrintActor.create(),
|
||||
"PrintActor",
|
||||
DispatcherSelector.fromConfig("my-pinned-dispatcher"));
|
||||
// #defining-pinned-dispatcher
|
||||
}
|
||||
|
||||
public void compileLookup() {
|
||||
// #lookup
|
||||
// this is scala.concurrent.ExecutionContextExecutor, which implements
|
||||
// both scala.concurrent.ExecutionContext (for use with Futures, Scheduler, etc.)
|
||||
// and java.util.concurrent.Executor (for use with CompletableFuture etc.)
|
||||
final ExecutionContextExecutor ex =
|
||||
system.dispatchers().lookup(DispatcherSelector.fromConfig("my-dispatcher"));
|
||||
// #lookup
|
||||
}
|
||||
}
|
||||
33
docs/src/test/java/jdocs/actor/typed/PrintActor.java
Normal file
33
docs/src/test/java/jdocs/actor/typed/PrintActor.java
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor.typed;
|
||||
|
||||
import org.apache.pekko.actor.typed.*;
|
||||
import org.apache.pekko.actor.typed.javadsl.*;
|
||||
|
||||
// #print-actor
|
||||
class PrintActor extends AbstractBehavior<Integer> {
|
||||
|
||||
public static Behavior<Integer> create() {
|
||||
return Behaviors.setup(PrintActor::new);
|
||||
}
|
||||
|
||||
private PrintActor(ActorContext<Integer> context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<Integer> createReceive() {
|
||||
return newReceiveBuilder()
|
||||
.onMessage(
|
||||
Integer.class,
|
||||
i -> {
|
||||
System.out.println("PrintActor: " + i);
|
||||
return Behaviors.same();
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #print-actor
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor.typed;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.apache.pekko.actor.typed.*;
|
||||
import org.apache.pekko.actor.typed.javadsl.*;
|
||||
|
||||
// #separate-dispatcher
|
||||
class SeparateDispatcherFutureActor extends AbstractBehavior<Integer> {
|
||||
private final Executor ec;
|
||||
|
||||
public static Behavior<Integer> create() {
|
||||
return Behaviors.setup(SeparateDispatcherFutureActor::new);
|
||||
}
|
||||
|
||||
private SeparateDispatcherFutureActor(ActorContext<Integer> context) {
|
||||
super(context);
|
||||
ec =
|
||||
context
|
||||
.getSystem()
|
||||
.dispatchers()
|
||||
.lookup(DispatcherSelector.fromConfig("my-blocking-dispatcher"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<Integer> createReceive() {
|
||||
return newReceiveBuilder()
|
||||
.onMessage(
|
||||
Integer.class,
|
||||
i -> {
|
||||
triggerFutureBlockingOperation(i, ec);
|
||||
return Behaviors.same();
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private static void triggerFutureBlockingOperation(Integer i, Executor ec) {
|
||||
System.out.println("Calling blocking Future on separate dispatcher: " + i);
|
||||
CompletableFuture<Integer> f =
|
||||
CompletableFuture.supplyAsync(
|
||||
() -> {
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
System.out.println("Blocking future finished: " + i);
|
||||
return i;
|
||||
} catch (InterruptedException e) {
|
||||
return -1;
|
||||
}
|
||||
},
|
||||
ec);
|
||||
}
|
||||
}
|
||||
// #separate-dispatcher
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.actor.typed;
|
||||
|
||||
import org.apache.pekko.actor.typed.ActorRef;
|
||||
import org.apache.pekko.actor.typed.Behavior;
|
||||
import org.apache.pekko.actor.typed.javadsl.AbstractBehavior;
|
||||
import org.apache.pekko.actor.typed.javadsl.ActorContext;
|
||||
import org.apache.pekko.actor.typed.javadsl.Receive;
|
||||
import org.apache.pekko.actor.typed.javadsl.AskPattern;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
interface SharedMutableStateDocTest {
|
||||
|
||||
static CompletableFuture<String> expensiveCalculation() {
|
||||
throw new UnsupportedOperationException("just a sample signature");
|
||||
}
|
||||
|
||||
class Query {
|
||||
public final ActorRef<String> replyTo;
|
||||
|
||||
public Query(ActorRef<String> replyTo) {
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
// #mutable-state
|
||||
class MyActor extends AbstractBehavior<MyActor.Command> {
|
||||
|
||||
interface Command {}
|
||||
|
||||
class Message implements Command {
|
||||
public final ActorRef<Object> otherActor;
|
||||
|
||||
public Message(ActorRef<Object> replyTo) {
|
||||
this.otherActor = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateState implements Command {
|
||||
public final String newState;
|
||||
|
||||
public UpdateState(String newState) {
|
||||
this.newState = newState;
|
||||
}
|
||||
}
|
||||
|
||||
private String state = "";
|
||||
private Set<String> mySet = new HashSet<>();
|
||||
|
||||
public MyActor(ActorContext<Command> context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<Command> createReceive() {
|
||||
return newReceiveBuilder()
|
||||
.onMessage(Message.class, this::onMessage)
|
||||
.onMessage(UpdateState.class, this::onUpdateState)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Behavior<Command> onMessage(Message message) {
|
||||
// Very bad: shared mutable object allows
|
||||
// the other actor to mutate your own state,
|
||||
// or worse, you might get weird race conditions
|
||||
message.otherActor.tell(mySet);
|
||||
|
||||
// Example of incorrect approach
|
||||
// Very bad: shared mutable state will cause your
|
||||
// application to break in weird ways
|
||||
CompletableFuture.runAsync(
|
||||
() -> {
|
||||
state = "This will race";
|
||||
});
|
||||
|
||||
// Example of incorrect approach
|
||||
// Very bad: shared mutable state will cause your
|
||||
// application to break in weird ways
|
||||
expensiveCalculation()
|
||||
.whenComplete(
|
||||
(result, failure) -> {
|
||||
if (result != null) state = "new state: " + result;
|
||||
});
|
||||
|
||||
// Example of correct approach
|
||||
// Turn the future result into a message that is sent to
|
||||
// self when future completes
|
||||
CompletableFuture<String> futureResult = expensiveCalculation();
|
||||
getContext()
|
||||
.pipeToSelf(
|
||||
futureResult,
|
||||
(result, failure) -> {
|
||||
if (result != null) return new UpdateState(result);
|
||||
else throw new RuntimeException(failure);
|
||||
});
|
||||
|
||||
// Another example of incorrect approach
|
||||
// mutating actor state from ask future callback
|
||||
CompletionStage<String> response =
|
||||
AskPattern.ask(
|
||||
message.otherActor,
|
||||
Query::new,
|
||||
Duration.ofSeconds(3),
|
||||
getContext().getSystem().scheduler());
|
||||
response.whenComplete(
|
||||
(result, failure) -> {
|
||||
if (result != null) state = "new state: " + result;
|
||||
});
|
||||
|
||||
// use context.ask instead, turns the completion
|
||||
// into a message sent to self
|
||||
getContext()
|
||||
.ask(
|
||||
String.class,
|
||||
message.otherActor,
|
||||
Duration.ofSeconds(3),
|
||||
Query::new,
|
||||
(result, failure) -> {
|
||||
if (result != null) return new UpdateState(result);
|
||||
else throw new RuntimeException(failure);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
private Behavior<Command> onUpdateState(UpdateState command) {
|
||||
// safe as long as `newState` is immutable, if it is mutable we'd need to
|
||||
// make a defensive copy
|
||||
this.state = command.newState;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
// #mutable-state
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.circuitbreaker;
|
||||
|
||||
// #imports1
|
||||
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
import java.time.Duration;
|
||||
import org.apache.pekko.pattern.CircuitBreaker;
|
||||
import org.apache.pekko.event.Logging;
|
||||
|
||||
import static org.apache.pekko.pattern.Patterns.pipe;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
// #imports1
|
||||
|
||||
// #circuit-breaker-initialization
|
||||
public class DangerousJavaActor extends AbstractActor {
|
||||
|
||||
private final CircuitBreaker breaker;
|
||||
private final LoggingAdapter log = Logging.getLogger(getContext().system(), this);
|
||||
|
||||
public DangerousJavaActor() {
|
||||
this.breaker =
|
||||
new CircuitBreaker(
|
||||
getContext().getDispatcher(),
|
||||
getContext().getSystem().getScheduler(),
|
||||
5,
|
||||
Duration.ofSeconds(10),
|
||||
Duration.ofMinutes(1))
|
||||
.addOnOpenListener(this::notifyMeOnOpen);
|
||||
}
|
||||
|
||||
public void notifyMeOnOpen() {
|
||||
log.warning("My CircuitBreaker is now open, and will not close for one minute");
|
||||
}
|
||||
// #circuit-breaker-initialization
|
||||
|
||||
// #circuit-breaker-usage
|
||||
public String dangerousCall() {
|
||||
return "This really isn't that dangerous of a call after all";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
"is my middle name"::equals,
|
||||
m ->
|
||||
pipe(
|
||||
breaker.callWithCircuitBreakerCS(
|
||||
() -> CompletableFuture.supplyAsync(this::dangerousCall)),
|
||||
getContext().getDispatcher())
|
||||
.to(sender()))
|
||||
.match(
|
||||
String.class,
|
||||
"block for me"::equals,
|
||||
m -> {
|
||||
sender().tell(breaker.callWithSyncCircuitBreaker(this::dangerousCall), self());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
// #circuit-breaker-usage
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.circuitbreaker;
|
||||
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.pattern.CircuitBreaker;
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
public class EvenNoFailureJavaExample extends AbstractActor {
|
||||
// #even-no-as-failure
|
||||
private final CircuitBreaker breaker;
|
||||
|
||||
public EvenNoFailureJavaExample() {
|
||||
this.breaker =
|
||||
new CircuitBreaker(
|
||||
getContext().getDispatcher(),
|
||||
getContext().getSystem().getScheduler(),
|
||||
5,
|
||||
Duration.ofSeconds(10),
|
||||
Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
public int luckyNumber() {
|
||||
BiFunction<Optional<Integer>, Optional<Throwable>, Boolean> evenNoAsFailure =
|
||||
(result, err) -> (result.isPresent() && result.get() % 2 == 0);
|
||||
|
||||
// this will return 8888 and increase failure count at the same time
|
||||
return this.breaker.callWithSyncCircuitBreaker(() -> 8888, evenNoAsFailure);
|
||||
}
|
||||
|
||||
// #even-no-as-failure
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.circuitbreaker;
|
||||
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ReceiveTimeout;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.event.Logging;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
import org.apache.pekko.pattern.CircuitBreaker;
|
||||
import java.time.Duration;
|
||||
|
||||
public class TellPatternJavaActor extends AbstractActor {
|
||||
|
||||
private final ActorRef target;
|
||||
private final CircuitBreaker breaker;
|
||||
private final LoggingAdapter log = Logging.getLogger(getContext().system(), this);
|
||||
|
||||
public TellPatternJavaActor(ActorRef targetActor) {
|
||||
this.target = targetActor;
|
||||
this.breaker =
|
||||
new CircuitBreaker(
|
||||
getContext().getDispatcher(),
|
||||
getContext().getSystem().getScheduler(),
|
||||
5,
|
||||
Duration.ofSeconds(10),
|
||||
Duration.ofMinutes(1))
|
||||
.addOnOpenListener(this::notifyMeOnOpen);
|
||||
}
|
||||
|
||||
public void notifyMeOnOpen() {
|
||||
log.warning("My CircuitBreaker is now open, and will not close for one minute");
|
||||
}
|
||||
|
||||
// #circuit-breaker-tell-pattern
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
payload -> "call".equals(payload) && breaker.isClosed(),
|
||||
payload -> target.tell("message", self()))
|
||||
.matchEquals("response", payload -> breaker.succeed())
|
||||
.match(Throwable.class, t -> breaker.fail())
|
||||
.match(ReceiveTimeout.class, t -> breaker.fail())
|
||||
.build();
|
||||
}
|
||||
// #circuit-breaker-tell-pattern
|
||||
|
||||
}
|
||||
78
docs/src/test/java/jdocs/cluster/ClusterDocTest.java
Normal file
78
docs/src/test/java/jdocs/cluster/ClusterDocTest.java
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (C) 2015-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
// #join-seed-nodes-imports
|
||||
import org.apache.pekko.actor.Address;
|
||||
import org.apache.pekko.cluster.Cluster;
|
||||
|
||||
// #join-seed-nodes-imports
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.cluster.Member;
|
||||
|
||||
public class ClusterDocTest extends AbstractJavaTest {
|
||||
|
||||
static ActorSystem system;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
system =
|
||||
ActorSystem.create(
|
||||
"ClusterDocTest",
|
||||
ConfigFactory.parseString(scala.docs.cluster.ClusterDocSpec.config()));
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDown() {
|
||||
TestKit.shutdownActorSystem(system);
|
||||
system = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void demonstrateLeave() {
|
||||
// #leave
|
||||
final Cluster cluster = Cluster.get(system);
|
||||
cluster.leave(cluster.selfAddress());
|
||||
// #leave
|
||||
|
||||
}
|
||||
|
||||
// compile only
|
||||
@SuppressWarnings("unused")
|
||||
public void demonstrateDataCenter() {
|
||||
// #dcAccess
|
||||
final Cluster cluster = Cluster.get(system);
|
||||
// this node's data center
|
||||
String dc = cluster.selfDataCenter();
|
||||
// all known data centers
|
||||
Set<String> allDc = cluster.state().getAllDataCenters();
|
||||
// a specific member's data center
|
||||
Member aMember = cluster.state().getMembers().iterator().next();
|
||||
String aDc = aMember.dataCenter();
|
||||
// #dcAccess
|
||||
}
|
||||
|
||||
// compile only
|
||||
@SuppressWarnings("unused")
|
||||
public void demonstrateJoinSeedNodes() {
|
||||
// #join-seed-nodes
|
||||
final Cluster cluster = Cluster.get(system);
|
||||
List<Address> list =
|
||||
new LinkedList<>(); // replace this with your method to dynamically get seed nodes
|
||||
cluster.joinSeedNodes(list);
|
||||
// #join-seed-nodes
|
||||
}
|
||||
}
|
||||
39
docs/src/test/java/jdocs/cluster/FactorialBackend.java
Normal file
39
docs/src/test/java/jdocs/cluster/FactorialBackend.java
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import static org.apache.pekko.pattern.Patterns.pipe;
|
||||
|
||||
// #backend
|
||||
public class FactorialBackend extends AbstractActor {
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Integer.class,
|
||||
n -> {
|
||||
CompletableFuture<FactorialResult> result =
|
||||
CompletableFuture.supplyAsync(() -> factorial(n))
|
||||
.thenApply((factorial) -> new FactorialResult(n, factorial));
|
||||
|
||||
pipe(result, getContext().dispatcher()).to(getSender());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
BigInteger factorial(int n) {
|
||||
BigInteger acc = BigInteger.ONE;
|
||||
for (int i = 1; i <= n; ++i) {
|
||||
acc = acc.multiply(BigInteger.valueOf(i));
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
// #backend
|
||||
119
docs/src/test/java/jdocs/cluster/FactorialFrontend.java
Normal file
119
docs/src/test/java/jdocs/cluster/FactorialFrontend.java
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.cluster.metrics.AdaptiveLoadBalancingGroup;
|
||||
import org.apache.pekko.cluster.metrics.AdaptiveLoadBalancingPool;
|
||||
import org.apache.pekko.cluster.metrics.HeapMetricsSelector;
|
||||
import org.apache.pekko.cluster.metrics.SystemLoadAverageMetricsSelector;
|
||||
import org.apache.pekko.cluster.routing.ClusterRouterGroup;
|
||||
import org.apache.pekko.cluster.routing.ClusterRouterGroupSettings;
|
||||
import org.apache.pekko.cluster.routing.ClusterRouterPool;
|
||||
import org.apache.pekko.cluster.routing.ClusterRouterPoolSettings;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ReceiveTimeout;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.event.Logging;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
import org.apache.pekko.routing.FromConfig;
|
||||
|
||||
// #frontend
|
||||
public class FactorialFrontend extends AbstractActor {
|
||||
final int upToN;
|
||||
final boolean repeat;
|
||||
|
||||
LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
|
||||
|
||||
ActorRef backend =
|
||||
getContext().actorOf(FromConfig.getInstance().props(), "factorialBackendRouter");
|
||||
|
||||
public FactorialFrontend(int upToN, boolean repeat) {
|
||||
this.upToN = upToN;
|
||||
this.repeat = repeat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preStart() {
|
||||
sendJobs();
|
||||
getContext().setReceiveTimeout(Duration.ofSeconds(10));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
FactorialResult.class,
|
||||
result -> {
|
||||
if (result.n == upToN) {
|
||||
log.debug("{}! = {}", result.n, result.factorial);
|
||||
if (repeat) sendJobs();
|
||||
else getContext().stop(getSelf());
|
||||
}
|
||||
})
|
||||
.match(
|
||||
ReceiveTimeout.class,
|
||||
x -> {
|
||||
log.info("Timeout");
|
||||
sendJobs();
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
void sendJobs() {
|
||||
log.info("Starting batch of factorials up to [{}]", upToN);
|
||||
for (int n = 1; n <= upToN; n++) {
|
||||
backend.tell(n, getSelf());
|
||||
}
|
||||
}
|
||||
}
|
||||
// #frontend
|
||||
|
||||
// not used, only for documentation
|
||||
abstract class FactorialFrontend2 extends AbstractActor {
|
||||
// #router-lookup-in-code
|
||||
int totalInstances = 100;
|
||||
Iterable<String> routeesPaths = Arrays.asList("/user/factorialBackend", "");
|
||||
boolean allowLocalRoutees = true;
|
||||
Set<String> useRoles = new HashSet<>(Arrays.asList("backend"));
|
||||
ActorRef backend =
|
||||
getContext()
|
||||
.actorOf(
|
||||
new ClusterRouterGroup(
|
||||
new AdaptiveLoadBalancingGroup(
|
||||
HeapMetricsSelector.getInstance(), Collections.<String>emptyList()),
|
||||
new ClusterRouterGroupSettings(
|
||||
totalInstances, routeesPaths, allowLocalRoutees, useRoles))
|
||||
.props(),
|
||||
"factorialBackendRouter2");
|
||||
|
||||
// #router-lookup-in-code
|
||||
}
|
||||
|
||||
// not used, only for documentation
|
||||
abstract class FactorialFrontend3 extends AbstractActor {
|
||||
// #router-deploy-in-code
|
||||
int totalInstances = 100;
|
||||
int maxInstancesPerNode = 3;
|
||||
boolean allowLocalRoutees = false;
|
||||
Set<String> useRoles = new HashSet<>(Arrays.asList("backend"));
|
||||
ActorRef backend =
|
||||
getContext()
|
||||
.actorOf(
|
||||
new ClusterRouterPool(
|
||||
new AdaptiveLoadBalancingPool(
|
||||
SystemLoadAverageMetricsSelector.getInstance(), 0),
|
||||
new ClusterRouterPoolSettings(
|
||||
totalInstances, maxInstancesPerNode, allowLocalRoutees, useRoles))
|
||||
.props(Props.create(FactorialBackend.class)),
|
||||
"factorialBackendRouter3");
|
||||
// #router-deploy-in-code
|
||||
}
|
||||
76
docs/src/test/java/jdocs/cluster/FactorialFrontendMain.java
Normal file
76
docs/src/test/java/jdocs/cluster/FactorialFrontendMain.java
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import com.typesafe.config.Config;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.cluster.Cluster;
|
||||
|
||||
public class FactorialFrontendMain {
|
||||
|
||||
public static void main(String[] args) {
|
||||
final int upToN = 200;
|
||||
|
||||
final Config config =
|
||||
ConfigFactory.parseString("akka.cluster.roles = [frontend]")
|
||||
.withFallback(ConfigFactory.load("factorial"));
|
||||
|
||||
final ActorSystem system = ActorSystem.create("ClusterSystem", config);
|
||||
system.log().info("Factorials will start when 2 backend members in the cluster.");
|
||||
// #registerOnUp
|
||||
Cluster.get(system)
|
||||
.registerOnMemberUp(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
system.actorOf(
|
||||
Props.create(FactorialFrontend.class, upToN, true), "factorialFrontend");
|
||||
}
|
||||
});
|
||||
// #registerOnUp
|
||||
|
||||
// #registerOnRemoved
|
||||
Cluster.get(system)
|
||||
.registerOnMemberRemoved(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// exit JVM when ActorSystem has been terminated
|
||||
final Runnable exit =
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
System.exit(0);
|
||||
}
|
||||
};
|
||||
system.registerOnTermination(exit);
|
||||
|
||||
// shut down ActorSystem
|
||||
system.terminate();
|
||||
|
||||
// In case ActorSystem shutdown takes longer than 10 seconds,
|
||||
// exit the JVM forcefully anyway.
|
||||
// We must spawn a separate thread to not block current thread,
|
||||
// since that would have blocked the shutdown of the ActorSystem.
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
system.getWhenTerminated().toCompletableFuture().get(10, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
System.exit(-1);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
});
|
||||
// #registerOnRemoved
|
||||
|
||||
}
|
||||
}
|
||||
19
docs/src/test/java/jdocs/cluster/FactorialResult.java
Normal file
19
docs/src/test/java/jdocs/cluster/FactorialResult.java
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.io.Serializable;
|
||||
|
||||
public class FactorialResult implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
public final int n;
|
||||
public final BigInteger factorial;
|
||||
|
||||
FactorialResult(int n, BigInteger factorial) {
|
||||
this.n = n;
|
||||
this.factorial = factorial;
|
||||
}
|
||||
}
|
||||
74
docs/src/test/java/jdocs/cluster/MetricsListener.java
Normal file
74
docs/src/test/java/jdocs/cluster/MetricsListener.java
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
// #metrics-listener
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.cluster.Cluster;
|
||||
import org.apache.pekko.cluster.ClusterEvent.CurrentClusterState;
|
||||
import org.apache.pekko.cluster.metrics.ClusterMetricsChanged;
|
||||
import org.apache.pekko.cluster.metrics.NodeMetrics;
|
||||
import org.apache.pekko.cluster.metrics.StandardMetrics;
|
||||
import org.apache.pekko.cluster.metrics.StandardMetrics.HeapMemory;
|
||||
import org.apache.pekko.cluster.metrics.StandardMetrics.Cpu;
|
||||
import org.apache.pekko.cluster.metrics.ClusterMetricsExtension;
|
||||
import org.apache.pekko.event.Logging;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
|
||||
public class MetricsListener extends AbstractActor {
|
||||
LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
|
||||
|
||||
Cluster cluster = Cluster.get(getContext().getSystem());
|
||||
|
||||
ClusterMetricsExtension extension = ClusterMetricsExtension.get(getContext().getSystem());
|
||||
|
||||
// Subscribe unto ClusterMetricsEvent events.
|
||||
@Override
|
||||
public void preStart() {
|
||||
extension.subscribe(getSelf());
|
||||
}
|
||||
|
||||
// Unsubscribe from ClusterMetricsEvent events.
|
||||
@Override
|
||||
public void postStop() {
|
||||
extension.unsubscribe(getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
ClusterMetricsChanged.class,
|
||||
clusterMetrics -> {
|
||||
for (NodeMetrics nodeMetrics : clusterMetrics.getNodeMetrics()) {
|
||||
if (nodeMetrics.address().equals(cluster.selfAddress())) {
|
||||
logHeap(nodeMetrics);
|
||||
logCpu(nodeMetrics);
|
||||
}
|
||||
}
|
||||
})
|
||||
.match(
|
||||
CurrentClusterState.class,
|
||||
message -> {
|
||||
// Ignore.
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
void logHeap(NodeMetrics nodeMetrics) {
|
||||
HeapMemory heap = StandardMetrics.extractHeapMemory(nodeMetrics);
|
||||
if (heap != null) {
|
||||
log.info("Used heap: {} MB", ((double) heap.used()) / 1024 / 1024);
|
||||
}
|
||||
}
|
||||
|
||||
void logCpu(NodeMetrics nodeMetrics) {
|
||||
Cpu cpu = StandardMetrics.extractCpu(nodeMetrics);
|
||||
if (cpu != null && cpu.systemLoadAverage().isDefined()) {
|
||||
log.info("Load: {} ({} processors)", cpu.systemLoadAverage().get(), cpu.processors());
|
||||
}
|
||||
}
|
||||
}
|
||||
// #metrics-listener
|
||||
61
docs/src/test/java/jdocs/cluster/SimpleClusterListener.java
Normal file
61
docs/src/test/java/jdocs/cluster/SimpleClusterListener.java
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.cluster.Cluster;
|
||||
import org.apache.pekko.cluster.ClusterEvent;
|
||||
import org.apache.pekko.cluster.ClusterEvent.MemberEvent;
|
||||
import org.apache.pekko.cluster.ClusterEvent.MemberUp;
|
||||
import org.apache.pekko.cluster.ClusterEvent.MemberRemoved;
|
||||
import org.apache.pekko.cluster.ClusterEvent.UnreachableMember;
|
||||
import org.apache.pekko.event.Logging;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
|
||||
public class SimpleClusterListener extends AbstractActor {
|
||||
LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
|
||||
Cluster cluster = Cluster.get(getContext().getSystem());
|
||||
|
||||
// subscribe to cluster changes
|
||||
@Override
|
||||
public void preStart() {
|
||||
// #subscribe
|
||||
cluster.subscribe(
|
||||
getSelf(), ClusterEvent.initialStateAsEvents(), MemberEvent.class, UnreachableMember.class);
|
||||
// #subscribe
|
||||
}
|
||||
|
||||
// re-subscribe when restart
|
||||
@Override
|
||||
public void postStop() {
|
||||
cluster.unsubscribe(getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
MemberUp.class,
|
||||
mUp -> {
|
||||
log.info("Member is Up: {}", mUp.member());
|
||||
})
|
||||
.match(
|
||||
UnreachableMember.class,
|
||||
mUnreachable -> {
|
||||
log.info("Member detected as unreachable: {}", mUnreachable.member());
|
||||
})
|
||||
.match(
|
||||
MemberRemoved.class,
|
||||
mRemoved -> {
|
||||
log.info("Member is Removed: {}", mRemoved.member());
|
||||
})
|
||||
.match(
|
||||
MemberEvent.class,
|
||||
message -> {
|
||||
// ignore
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
76
docs/src/test/java/jdocs/cluster/SimpleClusterListener2.java
Normal file
76
docs/src/test/java/jdocs/cluster/SimpleClusterListener2.java
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.cluster.Cluster;
|
||||
import org.apache.pekko.cluster.ClusterEvent.CurrentClusterState;
|
||||
import org.apache.pekko.cluster.ClusterEvent.MemberEvent;
|
||||
import org.apache.pekko.cluster.ClusterEvent.MemberUp;
|
||||
import org.apache.pekko.cluster.ClusterEvent.MemberRemoved;
|
||||
import org.apache.pekko.cluster.ClusterEvent.UnreachableMember;
|
||||
import org.apache.pekko.event.Logging;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
|
||||
public class SimpleClusterListener2 extends AbstractActor {
|
||||
LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
|
||||
// #join
|
||||
Cluster cluster = Cluster.get(getContext().getSystem());
|
||||
// #join
|
||||
|
||||
// subscribe to cluster changes
|
||||
@Override
|
||||
public void preStart() {
|
||||
// #join
|
||||
cluster.join(cluster.selfAddress());
|
||||
// #join
|
||||
|
||||
// #subscribe
|
||||
cluster.subscribe(getSelf(), MemberEvent.class, UnreachableMember.class);
|
||||
// #subscribe
|
||||
|
||||
// #register-on-memberup
|
||||
cluster.registerOnMemberUp(
|
||||
() -> cluster.subscribe(getSelf(), MemberEvent.class, UnreachableMember.class));
|
||||
// #register-on-memberup
|
||||
}
|
||||
|
||||
// re-subscribe when restart
|
||||
@Override
|
||||
public void postStop() {
|
||||
cluster.unsubscribe(getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
CurrentClusterState.class,
|
||||
state -> {
|
||||
log.info("Current members: {}", state.members());
|
||||
})
|
||||
.match(
|
||||
MemberUp.class,
|
||||
mUp -> {
|
||||
log.info("Member is Up: {}", mUp.member());
|
||||
})
|
||||
.match(
|
||||
UnreachableMember.class,
|
||||
mUnreachable -> {
|
||||
log.info("Member detected as unreachable: {}", mUnreachable.member());
|
||||
})
|
||||
.match(
|
||||
MemberRemoved.class,
|
||||
mRemoved -> {
|
||||
log.info("Member is Removed: {}", mRemoved.member());
|
||||
})
|
||||
.match(
|
||||
MemberEvent.class,
|
||||
event -> {
|
||||
// ignore
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
60
docs/src/test/java/jdocs/cluster/StatsAggregator.java
Normal file
60
docs/src/test/java/jdocs/cluster/StatsAggregator.java
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.time.Duration;
|
||||
|
||||
import jdocs.cluster.StatsMessages.JobFailed;
|
||||
import jdocs.cluster.StatsMessages.StatsResult;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ReceiveTimeout;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
|
||||
// #aggregator
|
||||
public class StatsAggregator extends AbstractActor {
|
||||
|
||||
final int expectedResults;
|
||||
final ActorRef replyTo;
|
||||
final List<Integer> results = new ArrayList<Integer>();
|
||||
|
||||
public StatsAggregator(int expectedResults, ActorRef replyTo) {
|
||||
this.expectedResults = expectedResults;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preStart() {
|
||||
getContext().setReceiveTimeout(Duration.ofSeconds(3));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Integer.class,
|
||||
wordCount -> {
|
||||
results.add(wordCount);
|
||||
if (results.size() == expectedResults) {
|
||||
int sum = 0;
|
||||
for (int c : results) {
|
||||
sum += c;
|
||||
}
|
||||
double meanWordLength = ((double) sum) / results.size();
|
||||
replyTo.tell(new StatsResult(meanWordLength), getSelf());
|
||||
getContext().stop(getSelf());
|
||||
}
|
||||
})
|
||||
.match(
|
||||
ReceiveTimeout.class,
|
||||
x -> {
|
||||
replyTo.tell(new JobFailed("Service unavailable, try again later"), getSelf());
|
||||
getContext().stop(getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #aggregator
|
||||
58
docs/src/test/java/jdocs/cluster/StatsMessages.java
Normal file
58
docs/src/test/java/jdocs/cluster/StatsMessages.java
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
// #messages
|
||||
public interface StatsMessages {
|
||||
|
||||
public static class StatsJob implements Serializable {
|
||||
private final String text;
|
||||
|
||||
public StatsJob(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
public static class StatsResult implements Serializable {
|
||||
private final double meanWordLength;
|
||||
|
||||
public StatsResult(double meanWordLength) {
|
||||
this.meanWordLength = meanWordLength;
|
||||
}
|
||||
|
||||
public double getMeanWordLength() {
|
||||
return meanWordLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "meanWordLength: " + meanWordLength;
|
||||
}
|
||||
}
|
||||
|
||||
public static class JobFailed implements Serializable {
|
||||
private final String reason;
|
||||
|
||||
public JobFailed(String reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JobFailed(" + reason + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
// #messages
|
||||
111
docs/src/test/java/jdocs/cluster/StatsSampleClient.java
Normal file
111
docs/src/test/java/jdocs/cluster/StatsSampleClient.java
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import jdocs.cluster.StatsMessages.JobFailed;
|
||||
import jdocs.cluster.StatsMessages.StatsJob;
|
||||
import jdocs.cluster.StatsMessages.StatsResult;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.time.Duration;
|
||||
import org.apache.pekko.actor.ActorSelection;
|
||||
import org.apache.pekko.actor.Address;
|
||||
import org.apache.pekko.actor.Cancellable;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.cluster.Cluster;
|
||||
import org.apache.pekko.cluster.ClusterEvent.UnreachableMember;
|
||||
import org.apache.pekko.cluster.ClusterEvent.ReachableMember;
|
||||
import org.apache.pekko.cluster.ClusterEvent.CurrentClusterState;
|
||||
import org.apache.pekko.cluster.ClusterEvent.MemberEvent;
|
||||
import org.apache.pekko.cluster.ClusterEvent.MemberUp;
|
||||
import org.apache.pekko.cluster.ClusterEvent.ReachabilityEvent;
|
||||
import org.apache.pekko.cluster.Member;
|
||||
import org.apache.pekko.cluster.MemberStatus;
|
||||
|
||||
public class StatsSampleClient extends AbstractActor {
|
||||
|
||||
final String servicePath;
|
||||
final Cancellable tickTask;
|
||||
final Set<Address> nodes = new HashSet<Address>();
|
||||
|
||||
Cluster cluster = Cluster.get(getContext().getSystem());
|
||||
|
||||
public StatsSampleClient(String servicePath) {
|
||||
this.servicePath = servicePath;
|
||||
Duration interval = Duration.ofMillis(2);
|
||||
tickTask =
|
||||
getContext()
|
||||
.getSystem()
|
||||
.scheduler()
|
||||
.scheduleWithFixedDelay(
|
||||
interval, interval, getSelf(), "tick", getContext().getDispatcher(), null);
|
||||
}
|
||||
|
||||
// subscribe to cluster changes, MemberEvent
|
||||
@Override
|
||||
public void preStart() {
|
||||
cluster.subscribe(getSelf(), MemberEvent.class, ReachabilityEvent.class);
|
||||
}
|
||||
|
||||
// re-subscribe when restart
|
||||
@Override
|
||||
public void postStop() {
|
||||
cluster.unsubscribe(getSelf());
|
||||
tickTask.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchEquals(
|
||||
"tick",
|
||||
x -> !nodes.isEmpty(),
|
||||
x -> {
|
||||
// just pick any one
|
||||
List<Address> nodesList = new ArrayList<Address>(nodes);
|
||||
Address address =
|
||||
nodesList.get(ThreadLocalRandom.current().nextInt(nodesList.size()));
|
||||
ActorSelection service = getContext().actorSelection(address + servicePath);
|
||||
service.tell(new StatsJob("this is the text that will be analyzed"), getSelf());
|
||||
})
|
||||
.match(StatsResult.class, System.out::println)
|
||||
.match(JobFailed.class, System.out::println)
|
||||
.match(
|
||||
CurrentClusterState.class,
|
||||
state -> {
|
||||
nodes.clear();
|
||||
for (Member member : state.getMembers()) {
|
||||
if (member.hasRole("compute") && member.status().equals(MemberStatus.up())) {
|
||||
nodes.add(member.address());
|
||||
}
|
||||
}
|
||||
})
|
||||
.match(
|
||||
MemberUp.class,
|
||||
mUp -> {
|
||||
if (mUp.member().hasRole("compute")) nodes.add(mUp.member().address());
|
||||
})
|
||||
.match(
|
||||
MemberEvent.class,
|
||||
event -> {
|
||||
nodes.remove(event.member().address());
|
||||
})
|
||||
.match(
|
||||
UnreachableMember.class,
|
||||
unreachable -> {
|
||||
nodes.remove(unreachable.member().address());
|
||||
})
|
||||
.match(
|
||||
ReachableMember.class,
|
||||
reachable -> {
|
||||
if (reachable.member().hasRole("compute")) nodes.add(reachable.member().address());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.Props;
|
||||
|
||||
public class StatsSampleOneMasterClientMain {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// note that client is not a compute node, role not defined
|
||||
ActorSystem system = ActorSystem.create("ClusterSystem", ConfigFactory.load("stats2"));
|
||||
system.actorOf(Props.create(StatsSampleClient.class, "/user/statsServiceProxy"), "client");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import com.typesafe.config.Config;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.PoisonPill;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.cluster.singleton.ClusterSingletonManager;
|
||||
import org.apache.pekko.cluster.singleton.ClusterSingletonManagerSettings;
|
||||
import org.apache.pekko.cluster.singleton.ClusterSingletonProxy;
|
||||
import org.apache.pekko.cluster.singleton.ClusterSingletonProxySettings;
|
||||
|
||||
public class StatsSampleOneMasterMain {
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length == 0) {
|
||||
startup(new String[] {"2551", "2552", "0"});
|
||||
StatsSampleOneMasterClientMain.main(new String[0]);
|
||||
} else {
|
||||
startup(args);
|
||||
}
|
||||
}
|
||||
|
||||
public static void startup(String[] ports) {
|
||||
for (String port : ports) {
|
||||
// Override the configuration of the port
|
||||
Config config =
|
||||
ConfigFactory.parseString("akka.remote.classic.netty.tcp.port=" + port)
|
||||
.withFallback(ConfigFactory.parseString("akka.cluster.roles = [compute]"))
|
||||
.withFallback(ConfigFactory.load("stats2"));
|
||||
|
||||
ActorSystem system = ActorSystem.create("ClusterSystem", config);
|
||||
|
||||
// #create-singleton-manager
|
||||
ClusterSingletonManagerSettings settings =
|
||||
ClusterSingletonManagerSettings.create(system).withRole("compute");
|
||||
system.actorOf(
|
||||
ClusterSingletonManager.props(
|
||||
Props.create(StatsService.class), PoisonPill.getInstance(), settings),
|
||||
"statsService");
|
||||
// #create-singleton-manager
|
||||
|
||||
// #singleton-proxy
|
||||
ClusterSingletonProxySettings proxySettings =
|
||||
ClusterSingletonProxySettings.create(system).withRole("compute");
|
||||
system.actorOf(
|
||||
ClusterSingletonProxy.props("/user/statsService", proxySettings), "statsServiceProxy");
|
||||
// #singleton-proxy
|
||||
}
|
||||
}
|
||||
}
|
||||
95
docs/src/test/java/jdocs/cluster/StatsService.java
Normal file
95
docs/src/test/java/jdocs/cluster/StatsService.java
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import org.apache.pekko.cluster.routing.ClusterRouterGroup;
|
||||
import org.apache.pekko.cluster.routing.ClusterRouterGroupSettings;
|
||||
import org.apache.pekko.cluster.routing.ClusterRouterPool;
|
||||
import org.apache.pekko.cluster.routing.ClusterRouterPoolSettings;
|
||||
import org.apache.pekko.routing.ConsistentHashingGroup;
|
||||
import org.apache.pekko.routing.ConsistentHashingPool;
|
||||
import jdocs.cluster.StatsMessages.StatsJob;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.routing.ConsistentHashingRouter.ConsistentHashableEnvelope;
|
||||
import org.apache.pekko.routing.FromConfig;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
// #service
|
||||
public class StatsService extends AbstractActor {
|
||||
|
||||
// This router is used both with lookup and deploy of routees. If you
|
||||
// have a router with only lookup of routees you can use Props.empty()
|
||||
// instead of Props.create(StatsWorker.class).
|
||||
ActorRef workerRouter =
|
||||
getContext()
|
||||
.actorOf(FromConfig.getInstance().props(Props.create(StatsWorker.class)), "workerRouter");
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
StatsJob.class,
|
||||
job -> !job.getText().isEmpty(),
|
||||
job -> {
|
||||
String[] words = job.getText().split(" ");
|
||||
ActorRef replyTo = getSender();
|
||||
|
||||
// create actor that collects replies from workers
|
||||
ActorRef aggregator =
|
||||
getContext().actorOf(Props.create(StatsAggregator.class, words.length, replyTo));
|
||||
|
||||
// send each word to a worker
|
||||
for (String word : words) {
|
||||
workerRouter.tell(new ConsistentHashableEnvelope(word, word), aggregator);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #service
|
||||
|
||||
// not used, only for documentation
|
||||
abstract class StatsService2 extends AbstractActor {
|
||||
// #router-lookup-in-code
|
||||
int totalInstances = 100;
|
||||
Iterable<String> routeesPaths = Collections.singletonList("/user/statsWorker");
|
||||
boolean allowLocalRoutees = true;
|
||||
Set<String> useRoles = new HashSet<>(Arrays.asList("compute"));
|
||||
ActorRef workerRouter =
|
||||
getContext()
|
||||
.actorOf(
|
||||
new ClusterRouterGroup(
|
||||
new ConsistentHashingGroup(routeesPaths),
|
||||
new ClusterRouterGroupSettings(
|
||||
totalInstances, routeesPaths, allowLocalRoutees, useRoles))
|
||||
.props(),
|
||||
"workerRouter2");
|
||||
// #router-lookup-in-code
|
||||
}
|
||||
|
||||
// not used, only for documentation
|
||||
abstract class StatsService3 extends AbstractActor {
|
||||
// #router-deploy-in-code
|
||||
int totalInstances = 100;
|
||||
int maxInstancesPerNode = 3;
|
||||
boolean allowLocalRoutees = false;
|
||||
Set<String> useRoles = new HashSet<>(Arrays.asList("compute"));
|
||||
ActorRef workerRouter =
|
||||
getContext()
|
||||
.actorOf(
|
||||
new ClusterRouterPool(
|
||||
new ConsistentHashingPool(0),
|
||||
new ClusterRouterPoolSettings(
|
||||
totalInstances, maxInstancesPerNode, allowLocalRoutees, useRoles))
|
||||
.props(Props.create(StatsWorker.class)),
|
||||
"workerRouter3");
|
||||
// #router-deploy-in-code
|
||||
}
|
||||
33
docs/src/test/java/jdocs/cluster/StatsWorker.java
Normal file
33
docs/src/test/java/jdocs/cluster/StatsWorker.java
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
|
||||
// #worker
|
||||
public class StatsWorker extends AbstractActor {
|
||||
|
||||
Map<String, Integer> cache = new HashMap<String, Integer>();
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
word -> {
|
||||
Integer length = cache.get(word);
|
||||
if (length == null) {
|
||||
length = word.length();
|
||||
cache.put(word, length);
|
||||
}
|
||||
getSender().tell(length, getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #worker
|
||||
66
docs/src/test/java/jdocs/cluster/TransformationBackend.java
Normal file
66
docs/src/test/java/jdocs/cluster/TransformationBackend.java
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import static jdocs.cluster.TransformationMessages.BACKEND_REGISTRATION;
|
||||
import jdocs.cluster.TransformationMessages.TransformationJob;
|
||||
import jdocs.cluster.TransformationMessages.TransformationResult;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.cluster.Cluster;
|
||||
import org.apache.pekko.cluster.ClusterEvent.CurrentClusterState;
|
||||
import org.apache.pekko.cluster.ClusterEvent.MemberUp;
|
||||
import org.apache.pekko.cluster.Member;
|
||||
import org.apache.pekko.cluster.MemberStatus;
|
||||
|
||||
// #backend
|
||||
public class TransformationBackend extends AbstractActor {
|
||||
|
||||
Cluster cluster = Cluster.get(getContext().getSystem());
|
||||
|
||||
// subscribe to cluster changes, MemberUp
|
||||
@Override
|
||||
public void preStart() {
|
||||
cluster.subscribe(getSelf(), MemberUp.class);
|
||||
}
|
||||
|
||||
// re-subscribe when restart
|
||||
@Override
|
||||
public void postStop() {
|
||||
cluster.unsubscribe(getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
TransformationJob.class,
|
||||
job -> {
|
||||
getSender().tell(new TransformationResult(job.getText().toUpperCase()), getSelf());
|
||||
})
|
||||
.match(
|
||||
CurrentClusterState.class,
|
||||
state -> {
|
||||
for (Member member : state.getMembers()) {
|
||||
if (member.status().equals(MemberStatus.up())) {
|
||||
register(member);
|
||||
}
|
||||
}
|
||||
})
|
||||
.match(
|
||||
MemberUp.class,
|
||||
mUp -> {
|
||||
register(mUp.member());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
void register(Member member) {
|
||||
if (member.hasRole("frontend"))
|
||||
getContext()
|
||||
.actorSelection(member.address() + "/user/frontend")
|
||||
.tell(BACKEND_REGISTRATION, getSelf());
|
||||
}
|
||||
}
|
||||
// #backend
|
||||
54
docs/src/test/java/jdocs/cluster/TransformationFrontend.java
Normal file
54
docs/src/test/java/jdocs/cluster/TransformationFrontend.java
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import static jdocs.cluster.TransformationMessages.BACKEND_REGISTRATION;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import jdocs.cluster.TransformationMessages.JobFailed;
|
||||
import jdocs.cluster.TransformationMessages.TransformationJob;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.Terminated;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
|
||||
// #frontend
|
||||
public class TransformationFrontend extends AbstractActor {
|
||||
|
||||
List<ActorRef> backends = new ArrayList<ActorRef>();
|
||||
int jobCounter = 0;
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
TransformationJob.class,
|
||||
job -> backends.isEmpty(),
|
||||
job -> {
|
||||
getSender()
|
||||
.tell(new JobFailed("Service unavailable, try again later", job), getSender());
|
||||
})
|
||||
.match(
|
||||
TransformationJob.class,
|
||||
job -> {
|
||||
jobCounter++;
|
||||
backends.get(jobCounter % backends.size()).forward(job, getContext());
|
||||
})
|
||||
.matchEquals(
|
||||
BACKEND_REGISTRATION,
|
||||
x -> {
|
||||
getContext().watch(getSender());
|
||||
backends.add(getSender());
|
||||
})
|
||||
.match(
|
||||
Terminated.class,
|
||||
terminated -> {
|
||||
backends.remove(terminated.getActor());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #frontend
|
||||
66
docs/src/test/java/jdocs/cluster/TransformationMessages.java
Normal file
66
docs/src/test/java/jdocs/cluster/TransformationMessages.java
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
// #messages
|
||||
public interface TransformationMessages {
|
||||
|
||||
public static class TransformationJob implements Serializable {
|
||||
private final String text;
|
||||
|
||||
public TransformationJob(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TransformationResult implements Serializable {
|
||||
private final String text;
|
||||
|
||||
public TransformationResult(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TransformationResult(" + text + ")";
|
||||
}
|
||||
}
|
||||
|
||||
public static class JobFailed implements Serializable {
|
||||
private final String reason;
|
||||
private final TransformationJob job;
|
||||
|
||||
public JobFailed(String reason, TransformationJob job) {
|
||||
this.reason = reason;
|
||||
this.job = job;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public TransformationJob getJob() {
|
||||
return job;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JobFailed(" + reason + ")";
|
||||
}
|
||||
}
|
||||
|
||||
public static final String BACKEND_REGISTRATION = "BackendRegistration";
|
||||
}
|
||||
// #messages
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (C) 2015-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster.singleton;
|
||||
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.actor.SupervisorStrategy;
|
||||
|
||||
// #singleton-supervisor-actor-usage-imports
|
||||
import org.apache.pekko.actor.PoisonPill;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.cluster.singleton.ClusterSingletonManager;
|
||||
import org.apache.pekko.cluster.singleton.ClusterSingletonManagerSettings;
|
||||
// #singleton-supervisor-actor-usage-imports
|
||||
|
||||
abstract class ClusterSingletonSupervision extends AbstractActor {
|
||||
|
||||
public ActorRef createSingleton(String name, Props props, SupervisorStrategy supervisorStrategy) {
|
||||
// #singleton-supervisor-actor-usage
|
||||
return getContext()
|
||||
.system()
|
||||
.actorOf(
|
||||
ClusterSingletonManager.props(
|
||||
Props.create(
|
||||
SupervisorActor.class, () -> new SupervisorActor(props, supervisorStrategy)),
|
||||
PoisonPill.getInstance(),
|
||||
ClusterSingletonManagerSettings.create(getContext().system())),
|
||||
name = name);
|
||||
// #singleton-supervisor-actor-usage
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (C) 2015-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.cluster.singleton;
|
||||
|
||||
// #singleton-supervisor-actor
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.actor.AbstractActor.Receive;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.actor.SupervisorStrategy;
|
||||
|
||||
public class SupervisorActor extends AbstractActor {
|
||||
final Props childProps;
|
||||
final SupervisorStrategy supervisorStrategy;
|
||||
final ActorRef child;
|
||||
|
||||
SupervisorActor(Props childProps, SupervisorStrategy supervisorStrategy) {
|
||||
this.childProps = childProps;
|
||||
this.supervisorStrategy = supervisorStrategy;
|
||||
this.child = getContext().actorOf(childProps, "supervised-child");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SupervisorStrategy supervisorStrategy() {
|
||||
return supervisorStrategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder().matchAny(msg -> child.forward(msg, getContext())).build();
|
||||
}
|
||||
}
|
||||
// #singleton-supervisor-actor
|
||||
70
docs/src/test/java/jdocs/config/ConfigDocTest.java
Normal file
70
docs/src/test/java/jdocs/config/ConfigDocTest.java
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.config;
|
||||
|
||||
// #imports
|
||||
import org.apache.pekko.actor.typed.ActorSystem;
|
||||
import org.apache.pekko.actor.typed.Behavior;
|
||||
import org.apache.pekko.actor.typed.javadsl.Behaviors;
|
||||
import com.typesafe.config.Config;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
|
||||
// #imports
|
||||
import org.apache.pekko.actor.testkit.typed.javadsl.ActorTestKit;
|
||||
|
||||
public class ConfigDocTest {
|
||||
|
||||
private Behavior<Void> rootBehavior = Behaviors.empty();
|
||||
|
||||
public void customConfig() {
|
||||
// #custom-config
|
||||
Config customConf = ConfigFactory.parseString("akka.log-config-on-start = on");
|
||||
// ConfigFactory.load sandwiches customConfig between default reference
|
||||
// config and default overrides, and then resolves it.
|
||||
ActorSystem<Void> system =
|
||||
ActorSystem.create(rootBehavior, "MySystem", ConfigFactory.load(customConf));
|
||||
// #custom-config
|
||||
|
||||
ActorTestKit.shutdown(system);
|
||||
}
|
||||
|
||||
public void compileOnlyPrintConfig() {
|
||||
// #dump-config
|
||||
ActorSystem<Void> system = ActorSystem.create(rootBehavior, "MySystem");
|
||||
system.logConfiguration();
|
||||
// #dump-config
|
||||
}
|
||||
|
||||
public void compileOnlySeparateApps() {
|
||||
// #separate-apps
|
||||
Config config = ConfigFactory.load();
|
||||
ActorSystem<Void> app1 =
|
||||
ActorSystem.create(rootBehavior, "MyApp1", config.getConfig("myapp1").withFallback(config));
|
||||
ActorSystem<Void> app2 =
|
||||
ActorSystem.create(
|
||||
rootBehavior,
|
||||
"MyApp2",
|
||||
config.getConfig("myapp2").withOnlyPath("akka").withFallback(config));
|
||||
// #separate-apps
|
||||
}
|
||||
|
||||
public ActorSystem createConfiguredSystem() {
|
||||
// #custom-config-2
|
||||
// make a Config with just your special setting
|
||||
Config myConfig = ConfigFactory.parseString("something=somethingElse");
|
||||
// load the normal config stack (system props,
|
||||
// then application.conf, then reference.conf)
|
||||
Config regularConfig = ConfigFactory.load();
|
||||
// override regular stack with myConfig
|
||||
Config combined = myConfig.withFallback(regularConfig);
|
||||
// put the result in between the overrides
|
||||
// (system props) and defaults again
|
||||
Config complete = ConfigFactory.load(combined);
|
||||
// create ActorSystem
|
||||
ActorSystem system = ActorSystem.create(rootBehavior, "myname", complete);
|
||||
// #custom-config-2
|
||||
return system;
|
||||
}
|
||||
}
|
||||
105
docs/src/test/java/jdocs/coordination/LeaseDocTest.java
Normal file
105
docs/src/test/java/jdocs/coordination/LeaseDocTest.java
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright (C) 2019-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.coordination;
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.coordination.lease.LeaseSettings;
|
||||
import org.apache.pekko.coordination.lease.javadsl.Lease;
|
||||
import org.apache.pekko.coordination.lease.javadsl.LeaseProvider;
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import docs.coordination.LeaseDocSpec;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.scalatestplus.junit.JUnitSuite;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class LeaseDocTest extends JUnitSuite {
|
||||
// #lease-example
|
||||
static class SampleLease extends Lease {
|
||||
|
||||
private LeaseSettings settings;
|
||||
|
||||
public SampleLease(LeaseSettings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LeaseSettings getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Boolean> acquire() {
|
||||
return CompletableFuture.completedFuture(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Boolean> acquire(Consumer<Optional<Throwable>> leaseLostCallback) {
|
||||
return CompletableFuture.completedFuture(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Boolean> release() {
|
||||
return CompletableFuture.completedFuture(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkLease() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// #lease-example
|
||||
|
||||
private static ActorSystem system;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
system = ActorSystem.create("LeaseDocTest", LeaseDocSpec.config());
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void teardown() {
|
||||
TestKit.shutdownActorSystem(system);
|
||||
system = null;
|
||||
}
|
||||
|
||||
private void doSomethingImportant(Optional<Throwable> leaseLostReason) {}
|
||||
|
||||
@Test
|
||||
public void javaLeaseBeLoadableFromJava() {
|
||||
// #lease-usage
|
||||
Lease lease =
|
||||
LeaseProvider.get(system).getLease("<name of the lease>", "jdocs-lease", "<owner name>");
|
||||
CompletionStage<Boolean> acquired = lease.acquire();
|
||||
boolean stillAcquired = lease.checkLease();
|
||||
CompletionStage<Boolean> released = lease.release();
|
||||
// #lease-usage
|
||||
|
||||
// #lost-callback
|
||||
lease.acquire(this::doSomethingImportant);
|
||||
// #lost-callback
|
||||
|
||||
// #cluster-owner
|
||||
// String owner = Cluster.get(system).selfAddress().hostPort();
|
||||
// #cluster-owner
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scalaLeaseBeLoadableFromJava() {
|
||||
Lease lease =
|
||||
LeaseProvider.get(system).getLease("<name of the lease>", "docs-lease", "<owner name>");
|
||||
CompletionStage<Boolean> acquired = lease.acquire();
|
||||
boolean stillAcquired = lease.checkLease();
|
||||
CompletionStage<Boolean> released = lease.release();
|
||||
lease.acquire(this::doSomethingImportant);
|
||||
}
|
||||
}
|
||||
103
docs/src/test/java/jdocs/ddata/DataBot.java
Normal file
103
docs/src/test/java/jdocs/ddata/DataBot.java
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright (C) 2015-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.ddata;
|
||||
|
||||
// #data-bot
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.Cancellable;
|
||||
import org.apache.pekko.cluster.Cluster;
|
||||
import org.apache.pekko.cluster.ddata.DistributedData;
|
||||
import org.apache.pekko.cluster.ddata.Key;
|
||||
import org.apache.pekko.cluster.ddata.ORSet;
|
||||
import org.apache.pekko.cluster.ddata.ORSetKey;
|
||||
import org.apache.pekko.cluster.ddata.Replicator;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.Changed;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.Subscribe;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.Update;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.UpdateResponse;
|
||||
import org.apache.pekko.cluster.ddata.SelfUniqueAddress;
|
||||
import org.apache.pekko.event.Logging;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
|
||||
public class DataBot extends AbstractActor {
|
||||
|
||||
private static final String TICK = "tick";
|
||||
|
||||
private final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
|
||||
|
||||
private final ActorRef replicator = DistributedData.get(getContext().getSystem()).replicator();
|
||||
private final SelfUniqueAddress node =
|
||||
DistributedData.get(getContext().getSystem()).selfUniqueAddress();
|
||||
|
||||
private final Cancellable tickTask =
|
||||
getContext()
|
||||
.getSystem()
|
||||
.scheduler()
|
||||
.scheduleWithFixedDelay(
|
||||
Duration.ofSeconds(5),
|
||||
Duration.ofSeconds(5),
|
||||
getSelf(),
|
||||
TICK,
|
||||
getContext().getDispatcher(),
|
||||
getSelf());
|
||||
|
||||
private final Key<ORSet<String>> dataKey = ORSetKey.create("key");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(String.class, a -> a.equals(TICK), a -> receiveTick())
|
||||
.match(
|
||||
Changed.class,
|
||||
c -> c.key().equals(dataKey),
|
||||
c -> receiveChanged((Changed<ORSet<String>>) c))
|
||||
.match(UpdateResponse.class, r -> receiveUpdateResponse())
|
||||
.build();
|
||||
}
|
||||
|
||||
private void receiveTick() {
|
||||
String s = String.valueOf((char) ThreadLocalRandom.current().nextInt(97, 123));
|
||||
if (ThreadLocalRandom.current().nextBoolean()) {
|
||||
// add
|
||||
log.info("Adding: {}", s);
|
||||
Update<ORSet<String>> update =
|
||||
new Update<>(dataKey, ORSet.create(), Replicator.writeLocal(), curr -> curr.add(node, s));
|
||||
replicator.tell(update, getSelf());
|
||||
} else {
|
||||
// remove
|
||||
log.info("Removing: {}", s);
|
||||
Update<ORSet<String>> update =
|
||||
new Update<>(
|
||||
dataKey, ORSet.create(), Replicator.writeLocal(), curr -> curr.remove(node, s));
|
||||
replicator.tell(update, getSelf());
|
||||
}
|
||||
}
|
||||
|
||||
private void receiveChanged(Changed<ORSet<String>> c) {
|
||||
ORSet<String> data = c.dataValue();
|
||||
log.info("Current elements: {}", data.getElements());
|
||||
}
|
||||
|
||||
private void receiveUpdateResponse() {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preStart() {
|
||||
Subscribe<ORSet<String>> subscribe = new Subscribe<>(dataKey, getSelf());
|
||||
replicator.tell(subscribe, ActorRef.noSender());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postStop() {
|
||||
tickTask.cancel();
|
||||
}
|
||||
}
|
||||
// #data-bot
|
||||
469
docs/src/test/java/jdocs/ddata/DistributedDataDocTest.java
Normal file
469
docs/src/test/java/jdocs/ddata/DistributedDataDocTest.java
Normal file
|
|
@ -0,0 +1,469 @@
|
|||
/*
|
||||
* Copyright (C) 2015-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.ddata;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.pekko.actor.*;
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
import docs.ddata.DistributedDataDocSpec;
|
||||
import jdocs.AbstractJavaTest;
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
|
||||
import org.apache.pekko.cluster.ddata.*;
|
||||
import org.apache.pekko.japi.pf.ReceiveBuilder;
|
||||
|
||||
import static org.apache.pekko.cluster.ddata.Replicator.*;
|
||||
|
||||
@SuppressWarnings({"unchecked", "unused"})
|
||||
public class DistributedDataDocTest extends AbstractJavaTest {
|
||||
|
||||
static ActorSystem system;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
system =
|
||||
ActorSystem.create(
|
||||
"DistributedDataDocTest", ConfigFactory.parseString(DistributedDataDocSpec.config()));
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDown() {
|
||||
TestKit.shutdownActorSystem(system);
|
||||
system = null;
|
||||
}
|
||||
|
||||
static
|
||||
// #update
|
||||
class DemonstrateUpdate extends AbstractActor {
|
||||
final SelfUniqueAddress node =
|
||||
DistributedData.get(getContext().getSystem()).selfUniqueAddress();
|
||||
final ActorRef replicator = DistributedData.get(getContext().getSystem()).replicator();
|
||||
|
||||
final Key<PNCounter> counter1Key = PNCounterKey.create("counter1");
|
||||
final Key<GSet<String>> set1Key = GSetKey.create("set1");
|
||||
final Key<ORSet<String>> set2Key = ORSetKey.create("set2");
|
||||
final Key<Flag> activeFlagKey = FlagKey.create("active");
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
ReceiveBuilder b = receiveBuilder();
|
||||
|
||||
b.matchEquals(
|
||||
"demonstrate update",
|
||||
msg -> {
|
||||
replicator.tell(
|
||||
new Replicator.Update<PNCounter>(
|
||||
counter1Key,
|
||||
PNCounter.create(),
|
||||
Replicator.writeLocal(),
|
||||
curr -> curr.increment(node, 1)),
|
||||
getSelf());
|
||||
|
||||
final WriteConsistency writeTo3 = new WriteTo(3, Duration.ofSeconds(1));
|
||||
replicator.tell(
|
||||
new Replicator.Update<GSet<String>>(
|
||||
set1Key, GSet.create(), writeTo3, curr -> curr.add("hello")),
|
||||
getSelf());
|
||||
|
||||
final WriteConsistency writeMajority = new WriteMajority(Duration.ofSeconds(5));
|
||||
replicator.tell(
|
||||
new Replicator.Update<ORSet<String>>(
|
||||
set2Key, ORSet.create(), writeMajority, curr -> curr.add(node, "hello")),
|
||||
getSelf());
|
||||
|
||||
final WriteConsistency writeAll = new WriteAll(Duration.ofSeconds(5));
|
||||
replicator.tell(
|
||||
new Replicator.Update<Flag>(
|
||||
activeFlagKey, Flag.create(), writeAll, curr -> curr.switchOn()),
|
||||
getSelf());
|
||||
});
|
||||
// #update
|
||||
|
||||
// #update-response1
|
||||
b.match(
|
||||
UpdateSuccess.class,
|
||||
a -> a.key().equals(counter1Key),
|
||||
a -> {
|
||||
// ok
|
||||
});
|
||||
// #update-response1
|
||||
|
||||
// #update-response2
|
||||
b.match(
|
||||
UpdateSuccess.class,
|
||||
a -> a.key().equals(set1Key),
|
||||
a -> {
|
||||
// ok
|
||||
})
|
||||
.match(
|
||||
UpdateTimeout.class,
|
||||
a -> a.key().equals(set1Key),
|
||||
a -> {
|
||||
// write to 3 nodes failed within 1.second
|
||||
});
|
||||
// #update-response2
|
||||
|
||||
// #update
|
||||
return b.build();
|
||||
}
|
||||
}
|
||||
// #update
|
||||
|
||||
static
|
||||
// #update-request-context
|
||||
class DemonstrateUpdateWithRequestContext extends AbstractActor {
|
||||
final SelfUniqueAddress node = DistributedData.get(system).selfUniqueAddress();
|
||||
final ActorRef replicator = DistributedData.get(getContext().getSystem()).replicator();
|
||||
|
||||
final WriteConsistency writeTwo = new WriteTo(2, Duration.ofSeconds(3));
|
||||
final Key<PNCounter> counter1Key = PNCounterKey.create("counter1");
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
a -> a.equals("increment"),
|
||||
a -> {
|
||||
// incoming command to increase the counter
|
||||
Optional<Object> reqContext = Optional.of(getSender());
|
||||
Replicator.Update<PNCounter> upd =
|
||||
new Replicator.Update<PNCounter>(
|
||||
counter1Key,
|
||||
PNCounter.create(),
|
||||
writeTwo,
|
||||
reqContext,
|
||||
curr -> curr.increment(node, 1));
|
||||
replicator.tell(upd, getSelf());
|
||||
})
|
||||
.match(
|
||||
UpdateSuccess.class,
|
||||
a -> a.key().equals(counter1Key),
|
||||
a -> {
|
||||
ActorRef replyTo = (ActorRef) a.getRequest().get();
|
||||
replyTo.tell("ack", getSelf());
|
||||
})
|
||||
.match(
|
||||
UpdateTimeout.class,
|
||||
a -> a.key().equals(counter1Key),
|
||||
a -> {
|
||||
ActorRef replyTo = (ActorRef) a.getRequest().get();
|
||||
replyTo.tell("nack", getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #update-request-context
|
||||
|
||||
static
|
||||
// #get
|
||||
class DemonstrateGet extends AbstractActor {
|
||||
final ActorRef replicator = DistributedData.get(getContext().getSystem()).replicator();
|
||||
|
||||
final Key<PNCounter> counter1Key = PNCounterKey.create("counter1");
|
||||
final Key<GSet<String>> set1Key = GSetKey.create("set1");
|
||||
final Key<ORSet<String>> set2Key = ORSetKey.create("set2");
|
||||
final Key<Flag> activeFlagKey = FlagKey.create("active");
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
ReceiveBuilder b = receiveBuilder();
|
||||
|
||||
b.matchEquals(
|
||||
"demonstrate get",
|
||||
msg -> {
|
||||
replicator.tell(
|
||||
new Replicator.Get<PNCounter>(counter1Key, Replicator.readLocal()), getSelf());
|
||||
|
||||
final ReadConsistency readFrom3 = new ReadFrom(3, Duration.ofSeconds(1));
|
||||
replicator.tell(new Replicator.Get<GSet<String>>(set1Key, readFrom3), getSelf());
|
||||
|
||||
final ReadConsistency readMajority = new ReadMajority(Duration.ofSeconds(5));
|
||||
replicator.tell(new Replicator.Get<ORSet<String>>(set2Key, readMajority), getSelf());
|
||||
|
||||
final ReadConsistency readAll = new ReadAll(Duration.ofSeconds(5));
|
||||
replicator.tell(new Replicator.Get<Flag>(activeFlagKey, readAll), getSelf());
|
||||
});
|
||||
// #get
|
||||
|
||||
// #get-response1
|
||||
b.match(
|
||||
GetSuccess.class,
|
||||
a -> a.key().equals(counter1Key),
|
||||
a -> {
|
||||
GetSuccess<PNCounter> g = a;
|
||||
BigInteger value = g.dataValue().getValue();
|
||||
})
|
||||
.match(
|
||||
NotFound.class,
|
||||
a -> a.key().equals(counter1Key),
|
||||
a -> {
|
||||
// key counter1 does not exist
|
||||
});
|
||||
// #get-response1
|
||||
|
||||
// #get-response2
|
||||
b.match(
|
||||
GetSuccess.class,
|
||||
a -> a.key().equals(set1Key),
|
||||
a -> {
|
||||
GetSuccess<GSet<String>> g = a;
|
||||
Set<String> value = g.dataValue().getElements();
|
||||
})
|
||||
.match(
|
||||
GetFailure.class,
|
||||
a -> a.key().equals(set1Key),
|
||||
a -> {
|
||||
// read from 3 nodes failed within 1.second
|
||||
})
|
||||
.match(
|
||||
NotFound.class,
|
||||
a -> a.key().equals(set1Key),
|
||||
a -> {
|
||||
// key set1 does not exist
|
||||
});
|
||||
// #get-response2
|
||||
|
||||
// #get
|
||||
return b.build();
|
||||
}
|
||||
}
|
||||
// #get
|
||||
|
||||
static
|
||||
// #get-request-context
|
||||
class DemonstrateGetWithRequestContext extends AbstractActor {
|
||||
final ActorRef replicator = DistributedData.get(getContext().getSystem()).replicator();
|
||||
|
||||
final ReadConsistency readTwo = new ReadFrom(2, Duration.ofSeconds(3));
|
||||
final Key<PNCounter> counter1Key = PNCounterKey.create("counter1");
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
a -> a.equals("get-count"),
|
||||
a -> {
|
||||
// incoming request to retrieve current value of the counter
|
||||
Optional<Object> reqContext = Optional.of(getSender());
|
||||
replicator.tell(new Replicator.Get<PNCounter>(counter1Key, readTwo), getSelf());
|
||||
})
|
||||
.match(
|
||||
GetSuccess.class,
|
||||
a -> a.key().equals(counter1Key),
|
||||
a -> {
|
||||
ActorRef replyTo = (ActorRef) a.getRequest().get();
|
||||
GetSuccess<PNCounter> g = a;
|
||||
long value = g.dataValue().getValue().longValue();
|
||||
replyTo.tell(value, getSelf());
|
||||
})
|
||||
.match(
|
||||
GetFailure.class,
|
||||
a -> a.key().equals(counter1Key),
|
||||
a -> {
|
||||
ActorRef replyTo = (ActorRef) a.getRequest().get();
|
||||
replyTo.tell(-1L, getSelf());
|
||||
})
|
||||
.match(
|
||||
NotFound.class,
|
||||
a -> a.key().equals(counter1Key),
|
||||
a -> {
|
||||
ActorRef replyTo = (ActorRef) a.getRequest().get();
|
||||
replyTo.tell(0L, getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #get-request-context
|
||||
|
||||
static
|
||||
// #subscribe
|
||||
class DemonstrateSubscribe extends AbstractActor {
|
||||
final ActorRef replicator = DistributedData.get(getContext().getSystem()).replicator();
|
||||
final Key<PNCounter> counter1Key = PNCounterKey.create("counter1");
|
||||
|
||||
BigInteger currentValue = BigInteger.valueOf(0);
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Changed.class,
|
||||
a -> a.key().equals(counter1Key),
|
||||
a -> {
|
||||
Changed<PNCounter> g = a;
|
||||
currentValue = g.dataValue().getValue();
|
||||
})
|
||||
.match(
|
||||
String.class,
|
||||
a -> a.equals("get-count"),
|
||||
a -> {
|
||||
// incoming request to retrieve current value of the counter
|
||||
getSender().tell(currentValue, getSender());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preStart() {
|
||||
// subscribe to changes of the Counter1Key value
|
||||
replicator.tell(new Subscribe<PNCounter>(counter1Key, getSelf()), ActorRef.noSender());
|
||||
}
|
||||
}
|
||||
// #subscribe
|
||||
|
||||
static
|
||||
// #delete
|
||||
class DemonstrateDelete extends AbstractActor {
|
||||
final ActorRef replicator = DistributedData.get(getContext().getSystem()).replicator();
|
||||
|
||||
final Key<PNCounter> counter1Key = PNCounterKey.create("counter1");
|
||||
final Key<ORSet<String>> set2Key = ORSetKey.create("set2");
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchEquals(
|
||||
"demonstrate delete",
|
||||
msg -> {
|
||||
replicator.tell(
|
||||
new Delete<PNCounter>(counter1Key, Replicator.writeLocal()), getSelf());
|
||||
|
||||
final WriteConsistency writeMajority = new WriteMajority(Duration.ofSeconds(5));
|
||||
replicator.tell(new Delete<PNCounter>(counter1Key, writeMajority), getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #delete
|
||||
|
||||
public void demonstratePNCounter() {
|
||||
// #pncounter
|
||||
final SelfUniqueAddress node = DistributedData.get(system).selfUniqueAddress();
|
||||
final PNCounter c0 = PNCounter.create();
|
||||
final PNCounter c1 = c0.increment(node, 1);
|
||||
final PNCounter c2 = c1.increment(node, 7);
|
||||
final PNCounter c3 = c2.decrement(node, 2);
|
||||
System.out.println(c3.value()); // 6
|
||||
// #pncounter
|
||||
}
|
||||
|
||||
public void demonstratePNCounterMap() {
|
||||
// #pncountermap
|
||||
final SelfUniqueAddress node = DistributedData.get(system).selfUniqueAddress();
|
||||
final PNCounterMap<String> m0 = PNCounterMap.create();
|
||||
final PNCounterMap<String> m1 = m0.increment(node, "a", 7);
|
||||
final PNCounterMap<String> m2 = m1.decrement(node, "a", 2);
|
||||
final PNCounterMap<String> m3 = m2.increment(node, "b", 1);
|
||||
System.out.println(m3.get("a")); // 5
|
||||
System.out.println(m3.getEntries());
|
||||
// #pncountermap
|
||||
}
|
||||
|
||||
public void demonstrateGSet() {
|
||||
// #gset
|
||||
final GSet<String> s0 = GSet.create();
|
||||
final GSet<String> s1 = s0.add("a");
|
||||
final GSet<String> s2 = s1.add("b").add("c");
|
||||
if (s2.contains("a")) System.out.println(s2.getElements()); // a, b, c
|
||||
// #gset
|
||||
}
|
||||
|
||||
public void demonstrateORSet() {
|
||||
// #orset
|
||||
final SelfUniqueAddress node = DistributedData.get(system).selfUniqueAddress();
|
||||
final ORSet<String> s0 = ORSet.create();
|
||||
final ORSet<String> s1 = s0.add(node, "a");
|
||||
final ORSet<String> s2 = s1.add(node, "b");
|
||||
final ORSet<String> s3 = s2.remove(node, "a");
|
||||
System.out.println(s3.getElements()); // b
|
||||
// #orset
|
||||
}
|
||||
|
||||
public void demonstrateORMultiMap() {
|
||||
// #ormultimap
|
||||
final SelfUniqueAddress node = DistributedData.get(system).selfUniqueAddress();
|
||||
final ORMultiMap<String, Integer> m0 = ORMultiMap.create();
|
||||
final ORMultiMap<String, Integer> m1 = m0.put(node, "a", new HashSet<>(Arrays.asList(1, 2, 3)));
|
||||
final ORMultiMap<String, Integer> m2 = m1.addBinding(node, "a", 4);
|
||||
final ORMultiMap<String, Integer> m3 = m2.removeBinding(node, "a", 2);
|
||||
final ORMultiMap<String, Integer> m4 = m3.addBinding(node, "b", 1);
|
||||
System.out.println(m4.getEntries());
|
||||
// #ormultimap
|
||||
}
|
||||
|
||||
public void demonstrateFlag() {
|
||||
// #flag
|
||||
final Flag f0 = Flag.create();
|
||||
final Flag f1 = f0.switchOn();
|
||||
System.out.println(f1.enabled());
|
||||
// #flag
|
||||
}
|
||||
|
||||
@Test
|
||||
public void demonstrateLWWRegister() {
|
||||
// #lwwregister
|
||||
final SelfUniqueAddress node = DistributedData.get(system).selfUniqueAddress();
|
||||
final LWWRegister<String> r1 = LWWRegister.create(node, "Hello");
|
||||
final LWWRegister<String> r2 = r1.withValue(node, "Hi");
|
||||
System.out.println(r1.value() + " by " + r1.updatedBy() + " at " + r1.timestamp());
|
||||
// #lwwregister
|
||||
assertEquals("Hi", r2.value());
|
||||
}
|
||||
|
||||
static
|
||||
// #lwwregister-custom-clock
|
||||
class Record {
|
||||
public final int version;
|
||||
public final String name;
|
||||
public final String address;
|
||||
|
||||
public Record(int version, String name, String address) {
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.address = address;
|
||||
}
|
||||
}
|
||||
|
||||
// #lwwregister-custom-clock
|
||||
|
||||
public void demonstrateLWWRegisterWithCustomClock() {
|
||||
// #lwwregister-custom-clock
|
||||
|
||||
final SelfUniqueAddress node = DistributedData.get(system).selfUniqueAddress();
|
||||
final LWWRegister.Clock<Record> recordClock =
|
||||
new LWWRegister.Clock<Record>() {
|
||||
@Override
|
||||
public long apply(long currentTimestamp, Record value) {
|
||||
return value.version;
|
||||
}
|
||||
};
|
||||
|
||||
final Record record1 = new Record(1, "Alice", "Union Square");
|
||||
final LWWRegister<Record> r1 = LWWRegister.create(node, record1);
|
||||
|
||||
final Record record2 = new Record(2, "Alice", "Madison Square");
|
||||
final LWWRegister<Record> r2 = LWWRegister.create(node, record2);
|
||||
|
||||
final LWWRegister<Record> r3 = r1.merge(r2);
|
||||
System.out.println(r3.value());
|
||||
// #lwwregister-custom-clock
|
||||
|
||||
assertEquals("Madison Square", r3.value().address);
|
||||
}
|
||||
}
|
||||
282
docs/src/test/java/jdocs/ddata/ShoppingCart.java
Normal file
282
docs/src/test/java/jdocs/ddata/ShoppingCart.java
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.ddata;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.cluster.ddata.*;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.GetFailure;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.GetResponse;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.GetSuccess;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.NotFound;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.ReadConsistency;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.ReadMajority;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.Update;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.UpdateFailure;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.UpdateSuccess;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.UpdateTimeout;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.WriteConsistency;
|
||||
import org.apache.pekko.cluster.ddata.Replicator.WriteMajority;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public class ShoppingCart extends AbstractActor {
|
||||
|
||||
// #read-write-majority
|
||||
private final WriteConsistency writeMajority = new WriteMajority(Duration.ofSeconds(3));
|
||||
private static final ReadConsistency readMajority = new ReadMajority(Duration.ofSeconds(3));
|
||||
// #read-write-majority
|
||||
|
||||
public static final String GET_CART = "getCart";
|
||||
|
||||
public static class AddItem {
|
||||
public final LineItem item;
|
||||
|
||||
public AddItem(LineItem item) {
|
||||
this.item = item;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RemoveItem {
|
||||
public final String productId;
|
||||
|
||||
public RemoveItem(String productId) {
|
||||
this.productId = productId;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Cart {
|
||||
public final Set<LineItem> items;
|
||||
|
||||
public Cart(Set<LineItem> items) {
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LineItem implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
public final String productId;
|
||||
public final String title;
|
||||
public final int quantity;
|
||||
|
||||
public LineItem(String productId, String title, int quantity) {
|
||||
this.productId = productId;
|
||||
this.title = title;
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((productId == null) ? 0 : productId.hashCode());
|
||||
result = prime * result + quantity;
|
||||
result = prime * result + ((title == null) ? 0 : title.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
LineItem other = (LineItem) obj;
|
||||
if (productId == null) {
|
||||
if (other.productId != null) return false;
|
||||
} else if (!productId.equals(other.productId)) return false;
|
||||
if (quantity != other.quantity) return false;
|
||||
if (title == null) {
|
||||
if (other.title != null) return false;
|
||||
} else if (!title.equals(other.title)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LineItem [productId="
|
||||
+ productId
|
||||
+ ", title="
|
||||
+ title
|
||||
+ ", quantity="
|
||||
+ quantity
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
|
||||
public static Props props(String userId) {
|
||||
return Props.create(ShoppingCart.class, userId);
|
||||
}
|
||||
|
||||
private final ActorRef replicator = DistributedData.get(getContext().getSystem()).replicator();
|
||||
private final SelfUniqueAddress node =
|
||||
DistributedData.get(getContext().getSystem()).selfUniqueAddress();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private final String userId;
|
||||
|
||||
private final Key<LWWMap<String, LineItem>> dataKey;
|
||||
|
||||
public ShoppingCart(String userId) {
|
||||
this.userId = userId;
|
||||
this.dataKey = LWWMapKey.create("cart-" + userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return matchGetCart().orElse(matchAddItem()).orElse(matchRemoveItem()).orElse(matchOther());
|
||||
}
|
||||
|
||||
// #get-cart
|
||||
private Receive matchGetCart() {
|
||||
return receiveBuilder()
|
||||
.matchEquals(GET_CART, s -> receiveGetCart())
|
||||
.match(
|
||||
GetSuccess.class,
|
||||
this::isResponseToGetCart,
|
||||
g -> receiveGetSuccess((GetSuccess<LWWMap<String, LineItem>>) g))
|
||||
.match(
|
||||
NotFound.class,
|
||||
this::isResponseToGetCart,
|
||||
n -> receiveNotFound((NotFound<LWWMap<String, LineItem>>) n))
|
||||
.match(
|
||||
GetFailure.class,
|
||||
this::isResponseToGetCart,
|
||||
f -> receiveGetFailure((GetFailure<LWWMap<String, LineItem>>) f))
|
||||
.build();
|
||||
}
|
||||
|
||||
private void receiveGetCart() {
|
||||
Optional<Object> ctx = Optional.of(getSender());
|
||||
replicator.tell(
|
||||
new Replicator.Get<LWWMap<String, LineItem>>(dataKey, readMajority, ctx), getSelf());
|
||||
}
|
||||
|
||||
private boolean isResponseToGetCart(GetResponse<?> response) {
|
||||
return response.key().equals(dataKey)
|
||||
&& (response.getRequest().orElse(null) instanceof ActorRef);
|
||||
}
|
||||
|
||||
private void receiveGetSuccess(GetSuccess<LWWMap<String, LineItem>> g) {
|
||||
Set<LineItem> items = new HashSet<>(g.dataValue().getEntries().values());
|
||||
ActorRef replyTo = (ActorRef) g.getRequest().get();
|
||||
replyTo.tell(new Cart(items), getSelf());
|
||||
}
|
||||
|
||||
private void receiveNotFound(NotFound<LWWMap<String, LineItem>> n) {
|
||||
ActorRef replyTo = (ActorRef) n.getRequest().get();
|
||||
replyTo.tell(new Cart(new HashSet<>()), getSelf());
|
||||
}
|
||||
|
||||
private void receiveGetFailure(GetFailure<LWWMap<String, LineItem>> f) {
|
||||
// ReadMajority failure, try again with local read
|
||||
Optional<Object> ctx = Optional.of(getSender());
|
||||
replicator.tell(
|
||||
new Replicator.Get<LWWMap<String, LineItem>>(dataKey, Replicator.readLocal(), ctx),
|
||||
getSelf());
|
||||
}
|
||||
// #get-cart
|
||||
|
||||
// #add-item
|
||||
private Receive matchAddItem() {
|
||||
return receiveBuilder().match(AddItem.class, this::receiveAddItem).build();
|
||||
}
|
||||
|
||||
private void receiveAddItem(AddItem add) {
|
||||
Update<LWWMap<String, LineItem>> update =
|
||||
new Update<>(dataKey, LWWMap.create(), writeMajority, cart -> updateCart(cart, add.item));
|
||||
replicator.tell(update, getSelf());
|
||||
}
|
||||
|
||||
// #add-item
|
||||
|
||||
private LWWMap<String, LineItem> updateCart(LWWMap<String, LineItem> data, LineItem item) {
|
||||
if (data.contains(item.productId)) {
|
||||
LineItem existingItem = data.get(item.productId).get();
|
||||
int newQuantity = existingItem.quantity + item.quantity;
|
||||
LineItem newItem = new LineItem(item.productId, item.title, newQuantity);
|
||||
return data.put(node, item.productId, newItem);
|
||||
} else {
|
||||
return data.put(node, item.productId, item);
|
||||
}
|
||||
}
|
||||
|
||||
private Receive matchRemoveItem() {
|
||||
return receiveBuilder()
|
||||
.match(RemoveItem.class, this::receiveRemoveItem)
|
||||
.match(
|
||||
GetSuccess.class,
|
||||
this::isResponseToRemoveItem,
|
||||
g -> receiveRemoveItemGetSuccess((GetSuccess<LWWMap<String, LineItem>>) g))
|
||||
.match(
|
||||
GetFailure.class,
|
||||
this::isResponseToRemoveItem,
|
||||
f -> receiveRemoveItemGetFailure((GetFailure<LWWMap<String, LineItem>>) f))
|
||||
.match(
|
||||
NotFound.class,
|
||||
this::isResponseToRemoveItem,
|
||||
n -> {
|
||||
/* nothing to remove */
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
// #remove-item
|
||||
private void receiveRemoveItem(RemoveItem rm) {
|
||||
// Try to fetch latest from a majority of nodes first, since ORMap
|
||||
// remove must have seen the item to be able to remove it.
|
||||
Optional<Object> ctx = Optional.of(rm);
|
||||
replicator.tell(
|
||||
new Replicator.Get<LWWMap<String, LineItem>>(dataKey, readMajority, ctx), getSelf());
|
||||
}
|
||||
|
||||
private void receiveRemoveItemGetSuccess(GetSuccess<LWWMap<String, LineItem>> g) {
|
||||
RemoveItem rm = (RemoveItem) g.getRequest().get();
|
||||
removeItem(rm.productId);
|
||||
}
|
||||
|
||||
private void receiveRemoveItemGetFailure(GetFailure<LWWMap<String, LineItem>> f) {
|
||||
// ReadMajority failed, fall back to best effort local value
|
||||
RemoveItem rm = (RemoveItem) f.getRequest().get();
|
||||
removeItem(rm.productId);
|
||||
}
|
||||
|
||||
private void removeItem(String productId) {
|
||||
Update<LWWMap<String, LineItem>> update =
|
||||
new Update<>(dataKey, LWWMap.create(), writeMajority, cart -> cart.remove(node, productId));
|
||||
replicator.tell(update, getSelf());
|
||||
}
|
||||
|
||||
private boolean isResponseToRemoveItem(GetResponse<?> response) {
|
||||
return response.key().equals(dataKey)
|
||||
&& (response.getRequest().orElse(null) instanceof RemoveItem);
|
||||
}
|
||||
// #remove-item
|
||||
|
||||
private Receive matchOther() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
UpdateSuccess.class,
|
||||
u -> {
|
||||
// ok
|
||||
})
|
||||
.match(
|
||||
UpdateTimeout.class,
|
||||
t -> {
|
||||
// will eventually be replicated
|
||||
})
|
||||
.match(
|
||||
UpdateFailure.class,
|
||||
f -> {
|
||||
throw new IllegalStateException("Unexpected failure: " + f);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
48
docs/src/test/java/jdocs/ddata/TwoPhaseSet.java
Normal file
48
docs/src/test/java/jdocs/ddata/TwoPhaseSet.java
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (C) 2015-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.ddata;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.pekko.cluster.ddata.AbstractReplicatedData;
|
||||
import org.apache.pekko.cluster.ddata.GSet;
|
||||
|
||||
// #twophaseset
|
||||
public class TwoPhaseSet extends AbstractReplicatedData<TwoPhaseSet> {
|
||||
|
||||
public final GSet<String> adds;
|
||||
public final GSet<String> removals;
|
||||
|
||||
public TwoPhaseSet(GSet<String> adds, GSet<String> removals) {
|
||||
this.adds = adds;
|
||||
this.removals = removals;
|
||||
}
|
||||
|
||||
public static TwoPhaseSet create() {
|
||||
return new TwoPhaseSet(GSet.create(), GSet.create());
|
||||
}
|
||||
|
||||
public TwoPhaseSet add(String element) {
|
||||
return new TwoPhaseSet(adds.add(element), removals);
|
||||
}
|
||||
|
||||
public TwoPhaseSet remove(String element) {
|
||||
return new TwoPhaseSet(adds, removals.add(element));
|
||||
}
|
||||
|
||||
public Set<String> getElements() {
|
||||
Set<String> result = new HashSet<>(adds.getElements());
|
||||
result.removeAll(removals.getElements());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TwoPhaseSet mergeData(TwoPhaseSet that) {
|
||||
return new TwoPhaseSet(this.adds.merge(that.adds), this.removals.merge(that.removals));
|
||||
}
|
||||
}
|
||||
// #twophaseset
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright (C) 2015-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.ddata.protobuf;
|
||||
|
||||
// #serializer
|
||||
import jdocs.ddata.TwoPhaseSet;
|
||||
import docs.ddata.protobuf.msg.TwoPhaseSetMessages;
|
||||
import docs.ddata.protobuf.msg.TwoPhaseSetMessages.TwoPhaseSet.Builder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.apache.pekko.actor.ExtendedActorSystem;
|
||||
import org.apache.pekko.cluster.ddata.GSet;
|
||||
import org.apache.pekko.cluster.ddata.protobuf.AbstractSerializationSupport;
|
||||
|
||||
public class TwoPhaseSetSerializer extends AbstractSerializationSupport {
|
||||
|
||||
private final ExtendedActorSystem system;
|
||||
|
||||
public TwoPhaseSetSerializer(ExtendedActorSystem system) {
|
||||
this.system = system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExtendedActorSystem system() {
|
||||
return this.system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean includeManifest() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int identifier() {
|
||||
return 99998;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBinary(Object obj) {
|
||||
if (obj instanceof TwoPhaseSet) {
|
||||
return twoPhaseSetToProto((TwoPhaseSet) obj).toByteArray();
|
||||
} else {
|
||||
throw new IllegalArgumentException("Can't serialize object of type " + obj.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromBinaryJava(byte[] bytes, Class<?> manifest) {
|
||||
return twoPhaseSetFromBinary(bytes);
|
||||
}
|
||||
|
||||
protected TwoPhaseSetMessages.TwoPhaseSet twoPhaseSetToProto(TwoPhaseSet twoPhaseSet) {
|
||||
Builder b = TwoPhaseSetMessages.TwoPhaseSet.newBuilder();
|
||||
ArrayList<String> adds = new ArrayList<>(twoPhaseSet.adds.getElements());
|
||||
if (!adds.isEmpty()) {
|
||||
Collections.sort(adds);
|
||||
b.addAllAdds(adds);
|
||||
}
|
||||
ArrayList<String> removals = new ArrayList<>(twoPhaseSet.removals.getElements());
|
||||
if (!removals.isEmpty()) {
|
||||
Collections.sort(removals);
|
||||
b.addAllRemovals(removals);
|
||||
}
|
||||
return b.build();
|
||||
}
|
||||
|
||||
protected TwoPhaseSet twoPhaseSetFromBinary(byte[] bytes) {
|
||||
try {
|
||||
TwoPhaseSetMessages.TwoPhaseSet msg = TwoPhaseSetMessages.TwoPhaseSet.parseFrom(bytes);
|
||||
GSet<String> adds = GSet.create();
|
||||
for (String elem : msg.getAddsList()) {
|
||||
adds = adds.add(elem);
|
||||
}
|
||||
GSet<String> removals = GSet.create();
|
||||
for (String elem : msg.getRemovalsList()) {
|
||||
removals = removals.add(elem);
|
||||
}
|
||||
// GSet will accumulate deltas when adding elements,
|
||||
// but those are not of interest in the result of the deserialization
|
||||
return new TwoPhaseSet(adds.resetDelta(), removals.resetDelta());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// #serializer
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright (C) 2015-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.ddata.protobuf;
|
||||
|
||||
// #serializer
|
||||
import jdocs.ddata.TwoPhaseSet;
|
||||
import docs.ddata.protobuf.msg.TwoPhaseSetMessages;
|
||||
import docs.ddata.protobuf.msg.TwoPhaseSetMessages.TwoPhaseSet2.Builder;
|
||||
|
||||
import org.apache.pekko.actor.ExtendedActorSystem;
|
||||
import org.apache.pekko.cluster.ddata.GSet;
|
||||
import org.apache.pekko.cluster.ddata.protobuf.AbstractSerializationSupport;
|
||||
import org.apache.pekko.cluster.ddata.protobuf.ReplicatedDataSerializer;
|
||||
|
||||
public class TwoPhaseSetSerializer2 extends AbstractSerializationSupport {
|
||||
|
||||
private final ExtendedActorSystem system;
|
||||
private final ReplicatedDataSerializer replicatedDataSerializer;
|
||||
|
||||
public TwoPhaseSetSerializer2(ExtendedActorSystem system) {
|
||||
this.system = system;
|
||||
this.replicatedDataSerializer = new ReplicatedDataSerializer(system);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExtendedActorSystem system() {
|
||||
return this.system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean includeManifest() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int identifier() {
|
||||
return 99998;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBinary(Object obj) {
|
||||
if (obj instanceof TwoPhaseSet) {
|
||||
return twoPhaseSetToProto((TwoPhaseSet) obj).toByteArray();
|
||||
} else {
|
||||
throw new IllegalArgumentException("Can't serialize object of type " + obj.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromBinaryJava(byte[] bytes, Class<?> manifest) {
|
||||
return twoPhaseSetFromBinary(bytes);
|
||||
}
|
||||
|
||||
protected TwoPhaseSetMessages.TwoPhaseSet2 twoPhaseSetToProto(TwoPhaseSet twoPhaseSet) {
|
||||
Builder b = TwoPhaseSetMessages.TwoPhaseSet2.newBuilder();
|
||||
if (!twoPhaseSet.adds.isEmpty())
|
||||
b.setAdds(otherMessageToProto(twoPhaseSet.adds).toByteString());
|
||||
if (!twoPhaseSet.removals.isEmpty())
|
||||
b.setRemovals(otherMessageToProto(twoPhaseSet.removals).toByteString());
|
||||
return b.build();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected TwoPhaseSet twoPhaseSetFromBinary(byte[] bytes) {
|
||||
try {
|
||||
TwoPhaseSetMessages.TwoPhaseSet2 msg = TwoPhaseSetMessages.TwoPhaseSet2.parseFrom(bytes);
|
||||
|
||||
GSet<String> adds = GSet.create();
|
||||
if (msg.hasAdds()) adds = (GSet<String>) otherMessageFromBinary(msg.getAdds().toByteArray());
|
||||
|
||||
GSet<String> removals = GSet.create();
|
||||
if (msg.hasRemovals())
|
||||
adds = (GSet<String>) otherMessageFromBinary(msg.getRemovals().toByteArray());
|
||||
|
||||
return new TwoPhaseSet(adds, removals);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// #serializer
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (C) 2015-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.ddata.protobuf;
|
||||
|
||||
import jdocs.ddata.TwoPhaseSet;
|
||||
|
||||
import org.apache.pekko.actor.ExtendedActorSystem;
|
||||
|
||||
public class TwoPhaseSetSerializerWithCompression extends TwoPhaseSetSerializer {
|
||||
public TwoPhaseSetSerializerWithCompression(ExtendedActorSystem system) {
|
||||
super(system);
|
||||
}
|
||||
|
||||
// #compression
|
||||
@Override
|
||||
public byte[] toBinary(Object obj) {
|
||||
if (obj instanceof TwoPhaseSet) {
|
||||
return compress(twoPhaseSetToProto((TwoPhaseSet) obj));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Can't serialize object of type " + obj.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromBinaryJava(byte[] bytes, Class<?> manifest) {
|
||||
return twoPhaseSetFromBinary(decompress(bytes));
|
||||
}
|
||||
// #compression
|
||||
}
|
||||
56
docs/src/test/java/jdocs/discovery/DnsDiscoveryDocTest.java
Normal file
56
docs/src/test/java/jdocs/discovery/DnsDiscoveryDocTest.java
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.discovery;
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
// #lookup-dns
|
||||
import org.apache.pekko.discovery.Discovery;
|
||||
import org.apache.pekko.discovery.ServiceDiscovery;
|
||||
// #lookup-dns
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import docs.discovery.DnsDiscoveryDocSpec;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.scalatestplus.junit.JUnitSuite;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DnsDiscoveryDocTest extends JUnitSuite {
|
||||
|
||||
private static ActorSystem system;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
system = ActorSystem.create("DnsDiscoveryDocTest", DnsDiscoveryDocSpec.config());
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void teardown() {
|
||||
TestKit.shutdownActorSystem(system);
|
||||
system = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dnsDiscoveryShouldResolveAkkaIo() throws Exception {
|
||||
try {
|
||||
// #lookup-dns
|
||||
|
||||
ServiceDiscovery discovery = Discovery.get(system).discovery();
|
||||
// ...
|
||||
CompletionStage<ServiceDiscovery.Resolved> result =
|
||||
discovery.lookup("foo", Duration.ofSeconds(3));
|
||||
// #lookup-dns
|
||||
|
||||
result.toCompletableFuture().get(5, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
system.log().warning("Failed lookup akka.io, but ignoring: " + e);
|
||||
// don't fail this test
|
||||
}
|
||||
}
|
||||
}
|
||||
269
docs/src/test/java/jdocs/dispatcher/DispatcherDocTest.java
Normal file
269
docs/src/test/java/jdocs/dispatcher/DispatcherDocTest.java
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.dispatcher;
|
||||
|
||||
import org.apache.pekko.dispatch.ControlMessage;
|
||||
import org.apache.pekko.dispatch.RequiresMessageQueue;
|
||||
import org.apache.pekko.testkit.AkkaSpec;
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
import docs.dispatcher.DispatcherDocSpec;
|
||||
import jdocs.AbstractJavaTest;
|
||||
import jdocs.actor.MyBoundedActor;
|
||||
import jdocs.actor.MyActor;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import scala.concurrent.ExecutionContextExecutor;
|
||||
|
||||
// #imports
|
||||
import org.apache.pekko.actor.*;
|
||||
// #imports
|
||||
// #imports-prio
|
||||
import org.apache.pekko.event.Logging;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
|
||||
// #imports-prio
|
||||
|
||||
// #imports-prio-mailbox
|
||||
import org.apache.pekko.dispatch.PriorityGenerator;
|
||||
import org.apache.pekko.dispatch.UnboundedStablePriorityMailbox;
|
||||
import org.apache.pekko.testkit.AkkaJUnitActorSystemResource;
|
||||
import com.typesafe.config.Config;
|
||||
|
||||
// #imports-prio-mailbox
|
||||
|
||||
// #imports-required-mailbox
|
||||
|
||||
// #imports-required-mailbox
|
||||
|
||||
public class DispatcherDocTest extends AbstractJavaTest {
|
||||
|
||||
@ClassRule
|
||||
public static AkkaJUnitActorSystemResource actorSystemResource =
|
||||
new AkkaJUnitActorSystemResource(
|
||||
"DispatcherDocTest",
|
||||
ConfigFactory.parseString(DispatcherDocSpec.javaConfig())
|
||||
.withFallback(ConfigFactory.parseString(DispatcherDocSpec.config()))
|
||||
.withFallback(AkkaSpec.testConf()));
|
||||
|
||||
private final ActorSystem system = actorSystemResource.getSystem();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void defineDispatcherInConfig() {
|
||||
// #defining-dispatcher-in-config
|
||||
ActorRef myActor = system.actorOf(Props.create(MyActor.class), "myactor");
|
||||
// #defining-dispatcher-in-config
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void defineDispatcherInCode() {
|
||||
// #defining-dispatcher-in-code
|
||||
ActorRef myActor =
|
||||
system.actorOf(Props.create(MyActor.class).withDispatcher("my-dispatcher"), "myactor3");
|
||||
// #defining-dispatcher-in-code
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void defineFixedPoolSizeDispatcher() {
|
||||
// #defining-fixed-pool-size-dispatcher
|
||||
ActorRef myActor =
|
||||
system.actorOf(Props.create(MyActor.class).withDispatcher("blocking-io-dispatcher"));
|
||||
// #defining-fixed-pool-size-dispatcher
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void definePinnedDispatcher() {
|
||||
// #defining-pinned-dispatcher
|
||||
ActorRef myActor =
|
||||
system.actorOf(Props.create(MyActor.class).withDispatcher("my-pinned-dispatcher"));
|
||||
// #defining-pinned-dispatcher
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void compileLookup() {
|
||||
// #lookup
|
||||
// this is scala.concurrent.ExecutionContextExecutor, which implements
|
||||
// both scala.concurrent.ExecutionContext (for use with Futures, Scheduler, etc.)
|
||||
// and java.util.concurrent.Executor (for use with CompletableFuture etc.)
|
||||
final ExecutionContextExecutor ex = system.dispatchers().lookup("my-dispatcher");
|
||||
// #lookup
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void defineMailboxInConfig() {
|
||||
// #defining-mailbox-in-config
|
||||
ActorRef myActor = system.actorOf(Props.create(MyActor.class), "priomailboxactor");
|
||||
// #defining-mailbox-in-config
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void defineMailboxInCode() {
|
||||
// #defining-mailbox-in-code
|
||||
ActorRef myActor = system.actorOf(Props.create(MyActor.class).withMailbox("prio-mailbox"));
|
||||
// #defining-mailbox-in-code
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void usingARequiredMailbox() {
|
||||
ActorRef myActor = system.actorOf(Props.create(MyBoundedActor.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void priorityDispatcher() throws Exception {
|
||||
TestKit probe = new TestKit(system);
|
||||
// #prio-dispatcher
|
||||
|
||||
class Demo extends AbstractActor {
|
||||
LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
|
||||
|
||||
{
|
||||
for (Object msg :
|
||||
new Object[] {
|
||||
"lowpriority",
|
||||
"lowpriority",
|
||||
"highpriority",
|
||||
"pigdog",
|
||||
"pigdog2",
|
||||
"pigdog3",
|
||||
"highpriority",
|
||||
PoisonPill.getInstance()
|
||||
}) {
|
||||
getSelf().tell(msg, getSelf());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchAny(
|
||||
message -> {
|
||||
log.info(message.toString());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// We create a new Actor that just prints out what it processes
|
||||
ActorRef myActor =
|
||||
system.actorOf(Props.create(Demo.class, this).withDispatcher("prio-dispatcher"));
|
||||
|
||||
/*
|
||||
Logs:
|
||||
'highpriority
|
||||
'highpriority
|
||||
'pigdog
|
||||
'pigdog2
|
||||
'pigdog3
|
||||
'lowpriority
|
||||
'lowpriority
|
||||
*/
|
||||
// #prio-dispatcher
|
||||
|
||||
probe.watch(myActor);
|
||||
probe.expectMsgClass(Terminated.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controlAwareDispatcher() throws Exception {
|
||||
TestKit probe = new TestKit(system);
|
||||
// #control-aware-dispatcher
|
||||
|
||||
class Demo extends AbstractActor {
|
||||
LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
|
||||
|
||||
{
|
||||
for (Object msg :
|
||||
new Object[] {"foo", "bar", new MyControlMessage(), PoisonPill.getInstance()}) {
|
||||
getSelf().tell(msg, getSelf());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchAny(
|
||||
message -> {
|
||||
log.info(message.toString());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// We create a new Actor that just prints out what it processes
|
||||
ActorRef myActor =
|
||||
system.actorOf(Props.create(Demo.class, this).withDispatcher("control-aware-dispatcher"));
|
||||
|
||||
/*
|
||||
Logs:
|
||||
'MyControlMessage
|
||||
'foo
|
||||
'bar
|
||||
*/
|
||||
// #control-aware-dispatcher
|
||||
|
||||
probe.watch(myActor);
|
||||
probe.expectMsgClass(Terminated.class);
|
||||
}
|
||||
|
||||
public
|
||||
// #prio-mailbox
|
||||
static class MyPrioMailbox extends UnboundedStablePriorityMailbox {
|
||||
// needed for reflective instantiation
|
||||
public MyPrioMailbox(ActorSystem.Settings settings, Config config) {
|
||||
// Create a new PriorityGenerator, lower prio means more important
|
||||
super(
|
||||
new PriorityGenerator() {
|
||||
@Override
|
||||
public int gen(Object message) {
|
||||
if (message.equals("highpriority"))
|
||||
return 0; // 'highpriority messages should be treated first if possible
|
||||
else if (message.equals("lowpriority"))
|
||||
return 2; // 'lowpriority messages should be treated last if possible
|
||||
else if (message.equals(PoisonPill.getInstance()))
|
||||
return 3; // PoisonPill when no other left
|
||||
else return 1; // By default they go between high and low prio
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// #prio-mailbox
|
||||
|
||||
public
|
||||
// #control-aware-mailbox-messages
|
||||
static class MyControlMessage implements ControlMessage {}
|
||||
// #control-aware-mailbox-messages
|
||||
|
||||
@Test
|
||||
public void requiredMailboxDispatcher() throws Exception {
|
||||
ActorRef myActor =
|
||||
system.actorOf(Props.create(MyActor.class).withDispatcher("custom-dispatcher"));
|
||||
}
|
||||
|
||||
public
|
||||
// #require-mailbox-on-actor
|
||||
static class MySpecialActor extends AbstractActor
|
||||
implements RequiresMessageQueue<MyUnboundedMessageQueueSemantics> {
|
||||
// #require-mailbox-on-actor
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return AbstractActor.emptyBehavior();
|
||||
}
|
||||
// #require-mailbox-on-actor
|
||||
// ...
|
||||
}
|
||||
// #require-mailbox-on-actor
|
||||
|
||||
@Test
|
||||
public void requiredMailboxActor() throws Exception {
|
||||
ActorRef myActor = system.actorOf(Props.create(MySpecialActor.class));
|
||||
}
|
||||
}
|
||||
60
docs/src/test/java/jdocs/dispatcher/MyUnboundedMailbox.java
Normal file
60
docs/src/test/java/jdocs/dispatcher/MyUnboundedMailbox.java
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.dispatcher;
|
||||
|
||||
// #mailbox-implementation-example
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.dispatch.Envelope;
|
||||
import org.apache.pekko.dispatch.MailboxType;
|
||||
import org.apache.pekko.dispatch.MessageQueue;
|
||||
import org.apache.pekko.dispatch.ProducesMessageQueue;
|
||||
import com.typesafe.config.Config;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.Queue;
|
||||
import scala.Option;
|
||||
|
||||
public class MyUnboundedMailbox
|
||||
implements MailboxType, ProducesMessageQueue<MyUnboundedMailbox.MyMessageQueue> {
|
||||
|
||||
// This is the MessageQueue implementation
|
||||
public static class MyMessageQueue implements MessageQueue, MyUnboundedMessageQueueSemantics {
|
||||
private final Queue<Envelope> queue = new ConcurrentLinkedQueue<Envelope>();
|
||||
|
||||
// these must be implemented; queue used as example
|
||||
public void enqueue(ActorRef receiver, Envelope handle) {
|
||||
queue.offer(handle);
|
||||
}
|
||||
|
||||
public Envelope dequeue() {
|
||||
return queue.poll();
|
||||
}
|
||||
|
||||
public int numberOfMessages() {
|
||||
return queue.size();
|
||||
}
|
||||
|
||||
public boolean hasMessages() {
|
||||
return !queue.isEmpty();
|
||||
}
|
||||
|
||||
public void cleanUp(ActorRef owner, MessageQueue deadLetters) {
|
||||
while (!queue.isEmpty()) {
|
||||
deadLetters.enqueue(owner, dequeue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This constructor signature must exist, it will be called by Akka
|
||||
public MyUnboundedMailbox(ActorSystem.Settings settings, Config config) {
|
||||
// put your initialization code here
|
||||
}
|
||||
|
||||
// The create method is called to create the MessageQueue
|
||||
public MessageQueue create(Option<ActorRef> owner, Option<ActorSystem> system) {
|
||||
return new MyMessageQueue();
|
||||
}
|
||||
}
|
||||
// #mailbox-implementation-example
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.dispatcher;
|
||||
|
||||
// #mailbox-marker-interface
|
||||
// Marker interface used for mailbox requirements mapping
|
||||
public interface MyUnboundedMessageQueueSemantics {}
|
||||
// #mailbox-marker-interface
|
||||
29
docs/src/test/java/jdocs/duration/Java.java
Normal file
29
docs/src/test/java/jdocs/duration/Java.java
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (C) 2013-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.duration;
|
||||
|
||||
// #import
|
||||
import scala.concurrent.duration.Duration;
|
||||
import scala.concurrent.duration.Deadline;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
// #import
|
||||
|
||||
class Java {
|
||||
public void demo() {
|
||||
// #dsl
|
||||
final Duration fivesec = Duration.create(5, "seconds");
|
||||
final Duration threemillis = Duration.create("3 millis");
|
||||
final Duration diff = fivesec.minus(threemillis);
|
||||
assertTrue(diff.lt(fivesec));
|
||||
assertTrue(Duration.Zero().lt(Duration.Inf()));
|
||||
// #dsl
|
||||
// #deadline
|
||||
final Deadline deadline = Duration.create(10, "seconds").fromNow();
|
||||
final Duration rest = deadline.timeLeft();
|
||||
// #deadline
|
||||
rest.toString();
|
||||
}
|
||||
}
|
||||
341
docs/src/test/java/jdocs/event/EventBusDocTest.java
Normal file
341
docs/src/test/java/jdocs/event/EventBusDocTest.java
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.event;
|
||||
|
||||
import org.apache.pekko.event.japi.EventBus;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.testkit.AkkaJUnitActorSystemResource;
|
||||
import org.apache.pekko.util.Subclassification;
|
||||
|
||||
// #lookup-bus
|
||||
import org.apache.pekko.event.japi.LookupEventBus;
|
||||
|
||||
// #lookup-bus
|
||||
|
||||
// #subchannel-bus
|
||||
import org.apache.pekko.event.japi.SubchannelEventBus;
|
||||
|
||||
// #subchannel-bus
|
||||
|
||||
// #scanning-bus
|
||||
import org.apache.pekko.event.japi.ScanningEventBus;
|
||||
|
||||
// #scanning-bus
|
||||
|
||||
// #actor-bus
|
||||
import org.apache.pekko.event.japi.ManagedActorEventBus;
|
||||
|
||||
// #actor-bus
|
||||
|
||||
public class EventBusDocTest extends AbstractJavaTest {
|
||||
|
||||
public static class Event {}
|
||||
|
||||
public static class Subscriber {}
|
||||
|
||||
public static class Classifier {}
|
||||
|
||||
public static interface EventBusApi extends EventBus<Event, Subscriber, Classifier> {
|
||||
|
||||
@Override
|
||||
// #event-bus-api
|
||||
/**
|
||||
* Attempts to register the subscriber to the specified Classifier
|
||||
*
|
||||
* @return true if successful and false if not (because it was already subscribed to that
|
||||
* Classifier, or otherwise)
|
||||
*/
|
||||
public boolean subscribe(Subscriber subscriber, Classifier to);
|
||||
|
||||
// #event-bus-api
|
||||
|
||||
@Override
|
||||
// #event-bus-api
|
||||
/**
|
||||
* Attempts to deregister the subscriber from the specified Classifier
|
||||
*
|
||||
* @return true if successful and false if not (because it wasn't subscribed to that Classifier,
|
||||
* or otherwise)
|
||||
*/
|
||||
public boolean unsubscribe(Subscriber subscriber, Classifier from);
|
||||
|
||||
// #event-bus-api
|
||||
|
||||
@Override
|
||||
// #event-bus-api
|
||||
/** Attempts to deregister the subscriber from all Classifiers it may be subscribed to */
|
||||
public void unsubscribe(Subscriber subscriber);
|
||||
|
||||
// #event-bus-api
|
||||
|
||||
@Override
|
||||
// #event-bus-api
|
||||
/** Publishes the specified Event to this bus */
|
||||
public void publish(Event event);
|
||||
|
||||
// #event-bus-api
|
||||
|
||||
}
|
||||
|
||||
public
|
||||
// #lookup-bus
|
||||
static class MsgEnvelope {
|
||||
public final String topic;
|
||||
public final Object payload;
|
||||
|
||||
public MsgEnvelope(String topic, Object payload) {
|
||||
this.topic = topic;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
// #lookup-bus
|
||||
public
|
||||
// #lookup-bus
|
||||
/**
|
||||
* Publishes the payload of the MsgEnvelope when the topic of the MsgEnvelope equals the String
|
||||
* specified when subscribing.
|
||||
*/
|
||||
static class LookupBusImpl extends LookupEventBus<MsgEnvelope, ActorRef, String> {
|
||||
|
||||
// is used for extracting the classifier from the incoming events
|
||||
@Override
|
||||
public String classify(MsgEnvelope event) {
|
||||
return event.topic;
|
||||
}
|
||||
|
||||
// will be invoked for each event for all subscribers which registered themselves
|
||||
// for the event’s classifier
|
||||
@Override
|
||||
public void publish(MsgEnvelope event, ActorRef subscriber) {
|
||||
subscriber.tell(event.payload, ActorRef.noSender());
|
||||
}
|
||||
|
||||
// must define a full order over the subscribers, expressed as expected from
|
||||
// `java.lang.Comparable.compare`
|
||||
@Override
|
||||
public int compareSubscribers(ActorRef a, ActorRef b) {
|
||||
return a.compareTo(b);
|
||||
}
|
||||
|
||||
// determines the initial size of the index data structure
|
||||
// used internally (i.e. the expected number of different classifiers)
|
||||
@Override
|
||||
public int mapSize() {
|
||||
return 128;
|
||||
}
|
||||
}
|
||||
// #lookup-bus
|
||||
|
||||
public
|
||||
// #subchannel-bus
|
||||
static class StartsWithSubclassification implements Subclassification<String> {
|
||||
@Override
|
||||
public boolean isEqual(String x, String y) {
|
||||
return x.equals(y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSubclass(String x, String y) {
|
||||
return x.startsWith(y);
|
||||
}
|
||||
}
|
||||
|
||||
// #subchannel-bus
|
||||
|
||||
public
|
||||
// #subchannel-bus
|
||||
/**
|
||||
* Publishes the payload of the MsgEnvelope when the topic of the MsgEnvelope starts with the
|
||||
* String specified when subscribing.
|
||||
*/
|
||||
static class SubchannelBusImpl extends SubchannelEventBus<MsgEnvelope, ActorRef, String> {
|
||||
|
||||
// Subclassification is an object providing `isEqual` and `isSubclass`
|
||||
// to be consumed by the other methods of this classifier
|
||||
@Override
|
||||
public Subclassification<String> subclassification() {
|
||||
return new StartsWithSubclassification();
|
||||
}
|
||||
|
||||
// is used for extracting the classifier from the incoming events
|
||||
@Override
|
||||
public String classify(MsgEnvelope event) {
|
||||
return event.topic;
|
||||
}
|
||||
|
||||
// will be invoked for each event for all subscribers which registered themselves
|
||||
// for the event’s classifier
|
||||
@Override
|
||||
public void publish(MsgEnvelope event, ActorRef subscriber) {
|
||||
subscriber.tell(event.payload, ActorRef.noSender());
|
||||
}
|
||||
}
|
||||
// #subchannel-bus
|
||||
|
||||
public
|
||||
// #scanning-bus
|
||||
/**
|
||||
* Publishes String messages with length less than or equal to the length specified when
|
||||
* subscribing.
|
||||
*/
|
||||
static class ScanningBusImpl extends ScanningEventBus<String, ActorRef, Integer> {
|
||||
|
||||
// is needed for determining matching classifiers and storing them in an
|
||||
// ordered collection
|
||||
@Override
|
||||
public int compareClassifiers(Integer a, Integer b) {
|
||||
return a.compareTo(b);
|
||||
}
|
||||
|
||||
// is needed for storing subscribers in an ordered collection
|
||||
@Override
|
||||
public int compareSubscribers(ActorRef a, ActorRef b) {
|
||||
return a.compareTo(b);
|
||||
}
|
||||
|
||||
// determines whether a given classifier shall match a given event; it is invoked
|
||||
// for each subscription for all received events, hence the name of the classifier
|
||||
@Override
|
||||
public boolean matches(Integer classifier, String event) {
|
||||
return event.length() <= classifier;
|
||||
}
|
||||
|
||||
// will be invoked for each event for all subscribers which registered themselves
|
||||
// for the event’s classifier
|
||||
@Override
|
||||
public void publish(String event, ActorRef subscriber) {
|
||||
subscriber.tell(event, ActorRef.noSender());
|
||||
}
|
||||
}
|
||||
// #scanning-bus
|
||||
|
||||
public
|
||||
// #actor-bus
|
||||
static class Notification {
|
||||
public final ActorRef ref;
|
||||
public final int id;
|
||||
|
||||
public Notification(ActorRef ref, int id) {
|
||||
this.ref = ref;
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
// #actor-bus
|
||||
|
||||
public
|
||||
// #actor-bus
|
||||
static class ActorBusImpl extends ManagedActorEventBus<Notification> {
|
||||
|
||||
// the ActorSystem will be used for book-keeping operations, such as subscribers terminating
|
||||
public ActorBusImpl(ActorSystem system) {
|
||||
super(system);
|
||||
}
|
||||
|
||||
// is used for extracting the classifier from the incoming events
|
||||
@Override
|
||||
public ActorRef classify(Notification event) {
|
||||
return event.ref;
|
||||
}
|
||||
|
||||
// determines the initial size of the index data structure
|
||||
// used internally (i.e. the expected number of different classifiers)
|
||||
@Override
|
||||
public int mapSize() {
|
||||
return 128;
|
||||
}
|
||||
}
|
||||
// #actor-bus
|
||||
|
||||
@ClassRule
|
||||
public static AkkaJUnitActorSystemResource actorSystemResource =
|
||||
new AkkaJUnitActorSystemResource("EventBusDocTest");
|
||||
|
||||
private final ActorSystem system = actorSystemResource.getSystem();
|
||||
|
||||
@Test
|
||||
public void demonstrateLookupClassification() {
|
||||
new TestKit(system) {
|
||||
{
|
||||
// #lookup-bus-test
|
||||
LookupBusImpl lookupBus = new LookupBusImpl();
|
||||
lookupBus.subscribe(getTestActor(), "greetings");
|
||||
lookupBus.publish(new MsgEnvelope("time", System.currentTimeMillis()));
|
||||
lookupBus.publish(new MsgEnvelope("greetings", "hello"));
|
||||
expectMsgEquals("hello");
|
||||
// #lookup-bus-test
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void demonstrateSubchannelClassification() {
|
||||
new TestKit(system) {
|
||||
{
|
||||
// #subchannel-bus-test
|
||||
SubchannelBusImpl subchannelBus = new SubchannelBusImpl();
|
||||
subchannelBus.subscribe(getTestActor(), "abc");
|
||||
subchannelBus.publish(new MsgEnvelope("xyzabc", "x"));
|
||||
subchannelBus.publish(new MsgEnvelope("bcdef", "b"));
|
||||
subchannelBus.publish(new MsgEnvelope("abc", "c"));
|
||||
expectMsgEquals("c");
|
||||
subchannelBus.publish(new MsgEnvelope("abcdef", "d"));
|
||||
expectMsgEquals("d");
|
||||
// #subchannel-bus-test
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void demonstrateScanningClassification() {
|
||||
new TestKit(system) {
|
||||
{
|
||||
// #scanning-bus-test
|
||||
ScanningBusImpl scanningBus = new ScanningBusImpl();
|
||||
scanningBus.subscribe(getTestActor(), 3);
|
||||
scanningBus.publish("xyzabc");
|
||||
scanningBus.publish("ab");
|
||||
expectMsgEquals("ab");
|
||||
scanningBus.publish("abc");
|
||||
expectMsgEquals("abc");
|
||||
// #scanning-bus-test
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void demonstrateManagedActorClassification() {
|
||||
// #actor-bus-test
|
||||
ActorRef observer1 = new TestKit(system).getRef();
|
||||
ActorRef observer2 = new TestKit(system).getRef();
|
||||
TestKit probe1 = new TestKit(system);
|
||||
TestKit probe2 = new TestKit(system);
|
||||
ActorRef subscriber1 = probe1.getRef();
|
||||
ActorRef subscriber2 = probe2.getRef();
|
||||
ActorBusImpl actorBus = new ActorBusImpl(system);
|
||||
actorBus.subscribe(subscriber1, observer1);
|
||||
actorBus.subscribe(subscriber2, observer1);
|
||||
actorBus.subscribe(subscriber2, observer2);
|
||||
Notification n1 = new Notification(observer1, 100);
|
||||
actorBus.publish(n1);
|
||||
probe1.expectMsgEquals(n1);
|
||||
probe2.expectMsgEquals(n1);
|
||||
Notification n2 = new Notification(observer2, 101);
|
||||
actorBus.publish(n2);
|
||||
probe2.expectMsgEquals(n2);
|
||||
probe1.expectNoMessage(Duration.ofMillis(500));
|
||||
// #actor-bus-test
|
||||
}
|
||||
}
|
||||
262
docs/src/test/java/jdocs/event/LoggingDocTest.java
Normal file
262
docs/src/test/java/jdocs/event/LoggingDocTest.java
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.event;
|
||||
|
||||
// #imports
|
||||
import org.apache.pekko.actor.*;
|
||||
import org.apache.pekko.event.Logging;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
|
||||
// #imports
|
||||
|
||||
// #imports-listener
|
||||
import org.apache.pekko.event.Logging.InitializeLogger;
|
||||
import org.apache.pekko.event.Logging.Error;
|
||||
import org.apache.pekko.event.Logging.Warning;
|
||||
import org.apache.pekko.event.Logging.Info;
|
||||
import org.apache.pekko.event.Logging.Debug;
|
||||
|
||||
// #imports-listener
|
||||
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import org.junit.Test;
|
||||
import java.util.Optional;
|
||||
|
||||
// #imports-mdc
|
||||
import org.apache.pekko.event.DiagnosticLoggingAdapter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
// #imports-mdc
|
||||
|
||||
// #imports-deadletter
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
// #imports-deadletter
|
||||
|
||||
public class LoggingDocTest extends AbstractJavaTest {
|
||||
|
||||
@Test
|
||||
public void useLoggingActor() {
|
||||
ActorSystem system = ActorSystem.create("MySystem");
|
||||
ActorRef myActor = system.actorOf(Props.create(MyActor.class, this));
|
||||
myActor.tell("test", ActorRef.noSender());
|
||||
TestKit.shutdownActorSystem(system);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void useLoggingActorWithMDC() {
|
||||
ActorSystem system = ActorSystem.create("MyDiagnosticSystem");
|
||||
ActorRef mdcActor = system.actorOf(Props.create(MdcActor.class, this));
|
||||
mdcActor.tell("some request", ActorRef.noSender());
|
||||
TestKit.shutdownActorSystem(system);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subscribeToDeadLetters() {
|
||||
// #deadletters
|
||||
final ActorSystem system = ActorSystem.create("DeadLetters");
|
||||
final ActorRef actor = system.actorOf(Props.create(DeadLetterActor.class));
|
||||
system.getEventStream().subscribe(actor, DeadLetter.class);
|
||||
// #deadletters
|
||||
TestKit.shutdownActorSystem(system);
|
||||
}
|
||||
|
||||
// #superclass-subscription-eventstream
|
||||
interface AllKindsOfMusic {}
|
||||
|
||||
class Jazz implements AllKindsOfMusic {
|
||||
public final String artist;
|
||||
|
||||
public Jazz(String artist) {
|
||||
this.artist = artist;
|
||||
}
|
||||
}
|
||||
|
||||
class Electronic implements AllKindsOfMusic {
|
||||
public final String artist;
|
||||
|
||||
public Electronic(String artist) {
|
||||
this.artist = artist;
|
||||
}
|
||||
}
|
||||
|
||||
static class Listener extends AbstractActor {
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Jazz.class,
|
||||
msg -> System.out.printf("%s is listening to: %s%n", getSelf().path().name(), msg))
|
||||
.match(
|
||||
Electronic.class,
|
||||
msg -> System.out.printf("%s is listening to: %s%n", getSelf().path().name(), msg))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #superclass-subscription-eventstream
|
||||
|
||||
@Test
|
||||
public void subscribeBySubclassification() {
|
||||
final ActorSystem system = ActorSystem.create("DeadLetters");
|
||||
// #superclass-subscription-eventstream
|
||||
final ActorRef actor = system.actorOf(Props.create(DeadLetterActor.class));
|
||||
system.getEventStream().subscribe(actor, DeadLetter.class);
|
||||
|
||||
final ActorRef jazzListener = system.actorOf(Props.create(Listener.class));
|
||||
final ActorRef musicListener = system.actorOf(Props.create(Listener.class));
|
||||
system.getEventStream().subscribe(jazzListener, Jazz.class);
|
||||
system.getEventStream().subscribe(musicListener, AllKindsOfMusic.class);
|
||||
|
||||
// only musicListener gets this message, since it listens to *all* kinds of music:
|
||||
system.getEventStream().publish(new Electronic("Parov Stelar"));
|
||||
|
||||
// jazzListener and musicListener will be notified about Jazz:
|
||||
system.getEventStream().publish(new Jazz("Sonny Rollins"));
|
||||
|
||||
// #superclass-subscription-eventstream
|
||||
TestKit.shutdownActorSystem(system);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subscribeToSuppressedDeadLetters() {
|
||||
final ActorSystem system = ActorSystem.create("SuppressedDeadLetters");
|
||||
final ActorRef actor = system.actorOf(Props.create(DeadLetterActor.class));
|
||||
|
||||
// #suppressed-deadletters
|
||||
system.getEventStream().subscribe(actor, SuppressedDeadLetter.class);
|
||||
// #suppressed-deadletters
|
||||
|
||||
TestKit.shutdownActorSystem(system);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subscribeToAllDeadLetters() {
|
||||
final ActorSystem system = ActorSystem.create("AllDeadLetters");
|
||||
final ActorRef actor = system.actorOf(Props.create(DeadLetterActor.class));
|
||||
|
||||
// #all-deadletters
|
||||
system.getEventStream().subscribe(actor, AllDeadLetters.class);
|
||||
// #all-deadletters
|
||||
|
||||
TestKit.shutdownActorSystem(system);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void demonstrateMultipleArgs() {
|
||||
final ActorSystem system = ActorSystem.create("multiArg");
|
||||
// #array
|
||||
final Object[] args = new Object[] {"The", "brown", "fox", "jumps", 42};
|
||||
system.log().debug("five parameters: {}, {}, {}, {}, {}", args);
|
||||
// #array
|
||||
TestKit.shutdownActorSystem(system);
|
||||
}
|
||||
|
||||
// #my-actor
|
||||
class MyActor extends AbstractActor {
|
||||
LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
|
||||
|
||||
@Override
|
||||
public void preStart() {
|
||||
log.debug("Starting");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preRestart(Throwable reason, Optional<Object> message) {
|
||||
log.error(
|
||||
reason,
|
||||
"Restarting due to [{}] when processing [{}]",
|
||||
reason.getMessage(),
|
||||
message.isPresent() ? message.get() : "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchEquals("test", msg -> log.info("Received test"))
|
||||
.matchAny(msg -> log.warning("Received unknown message: {}", msg))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// #my-actor
|
||||
|
||||
// #mdc-actor
|
||||
class MdcActor extends AbstractActor {
|
||||
|
||||
final DiagnosticLoggingAdapter log = Logging.getLogger(this);
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchAny(
|
||||
msg -> {
|
||||
Map<String, Object> mdc;
|
||||
mdc = new HashMap<String, Object>();
|
||||
mdc.put("requestId", 1234);
|
||||
mdc.put("visitorId", 5678);
|
||||
log.setMDC(mdc);
|
||||
|
||||
log.info("Starting new request");
|
||||
|
||||
log.clearMDC();
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// #mdc-actor
|
||||
|
||||
// #my-event-listener
|
||||
class MyEventListener extends AbstractActor {
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
InitializeLogger.class,
|
||||
msg -> {
|
||||
getSender().tell(Logging.loggerInitialized(), getSelf());
|
||||
})
|
||||
.match(
|
||||
Error.class,
|
||||
msg -> {
|
||||
// ...
|
||||
})
|
||||
.match(
|
||||
Warning.class,
|
||||
msg -> {
|
||||
// ...
|
||||
})
|
||||
.match(
|
||||
Info.class,
|
||||
msg -> {
|
||||
// ...
|
||||
})
|
||||
.match(
|
||||
Debug.class,
|
||||
msg -> {
|
||||
// ...
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #my-event-listener
|
||||
|
||||
public
|
||||
// #deadletter-actor
|
||||
static class DeadLetterActor extends AbstractActor {
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
DeadLetter.class,
|
||||
msg -> {
|
||||
System.out.println(msg);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #deadletter-actor
|
||||
}
|
||||
90
docs/src/test/java/jdocs/extension/ExtensionDocTest.java
Normal file
90
docs/src/test/java/jdocs/extension/ExtensionDocTest.java
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.extension;
|
||||
|
||||
// #imports
|
||||
import org.apache.pekko.actor.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
// #imports
|
||||
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ExtensionDocTest extends AbstractJavaTest {
|
||||
|
||||
public
|
||||
// #extension
|
||||
static class CountExtensionImpl implements Extension {
|
||||
// Since this Extension is a shared instance
|
||||
// per ActorSystem we need to be threadsafe
|
||||
private final AtomicLong counter = new AtomicLong(0);
|
||||
|
||||
// This is the operation this Extension provides
|
||||
public long increment() {
|
||||
return counter.incrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
// #extension
|
||||
|
||||
public
|
||||
// #extensionid
|
||||
static class CountExtension extends AbstractExtensionId<CountExtensionImpl>
|
||||
implements ExtensionIdProvider {
|
||||
// This will be the identifier of our CountExtension
|
||||
public static final CountExtension CountExtensionProvider = new CountExtension();
|
||||
|
||||
private CountExtension() {}
|
||||
|
||||
// The lookup method is required by ExtensionIdProvider,
|
||||
// so we return ourselves here, this allows us
|
||||
// to configure our extension to be loaded when
|
||||
// the ActorSystem starts up
|
||||
public CountExtension lookup() {
|
||||
return CountExtension.CountExtensionProvider; // The public static final
|
||||
}
|
||||
|
||||
// This method will be called by Akka
|
||||
// to instantiate our Extension
|
||||
public CountExtensionImpl createExtension(ExtendedActorSystem system) {
|
||||
return new CountExtensionImpl();
|
||||
}
|
||||
}
|
||||
|
||||
// #extensionid
|
||||
|
||||
public
|
||||
// #extension-usage-actor
|
||||
static class MyActor extends AbstractActor {
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchAny(
|
||||
msg -> {
|
||||
// typically you would use static import of the
|
||||
// CountExtension.CountExtensionProvider field
|
||||
CountExtension.CountExtensionProvider.get(getContext().getSystem()).increment();
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// #extension-usage-actor
|
||||
|
||||
@Test
|
||||
public void demonstrateHowToCreateAndUseAnAkkaExtensionInJava() {
|
||||
final ActorSystem system = null;
|
||||
try {
|
||||
// #extension-usage
|
||||
// typically you would use static import of the
|
||||
// CountExtension.CountExtensionProvider field
|
||||
CountExtension.CountExtensionProvider.get(system).increment();
|
||||
// #extension-usage
|
||||
} catch (Exception e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.extension;
|
||||
|
||||
// #imports
|
||||
import org.apache.pekko.actor.Extension;
|
||||
import org.apache.pekko.actor.AbstractExtensionId;
|
||||
import org.apache.pekko.actor.ExtensionIdProvider;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.ExtendedActorSystem;
|
||||
import com.typesafe.config.Config;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.time.Duration;
|
||||
|
||||
// #imports
|
||||
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SettingsExtensionDocTest extends AbstractJavaTest {
|
||||
|
||||
public
|
||||
// #extension
|
||||
static class SettingsImpl implements Extension {
|
||||
|
||||
public final String DB_URI;
|
||||
public final Duration CIRCUIT_BREAKER_TIMEOUT;
|
||||
|
||||
public SettingsImpl(Config config) {
|
||||
DB_URI = config.getString("myapp.db.uri");
|
||||
CIRCUIT_BREAKER_TIMEOUT =
|
||||
Duration.ofMillis(
|
||||
config.getDuration("myapp.circuit-breaker.timeout", TimeUnit.MILLISECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
// #extension
|
||||
|
||||
public
|
||||
// #extensionid
|
||||
static class Settings extends AbstractExtensionId<SettingsImpl> implements ExtensionIdProvider {
|
||||
public static final Settings SettingsProvider = new Settings();
|
||||
|
||||
private Settings() {}
|
||||
|
||||
public Settings lookup() {
|
||||
return Settings.SettingsProvider;
|
||||
}
|
||||
|
||||
public SettingsImpl createExtension(ExtendedActorSystem system) {
|
||||
return new SettingsImpl(system.settings().config());
|
||||
}
|
||||
}
|
||||
|
||||
// #extensionid
|
||||
|
||||
public
|
||||
// #extension-usage-actor
|
||||
static class MyActor extends AbstractActor {
|
||||
// typically you would use static import of the Settings.SettingsProvider field
|
||||
final SettingsImpl settings = Settings.SettingsProvider.get(getContext().getSystem());
|
||||
Connection connection = connect(settings.DB_URI, settings.CIRCUIT_BREAKER_TIMEOUT);
|
||||
|
||||
// #extension-usage-actor
|
||||
|
||||
public Connection connect(String dbUri, Duration circuitBreakerTimeout) {
|
||||
return new Connection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return AbstractActor.emptyBehavior();
|
||||
}
|
||||
// #extension-usage-actor
|
||||
}
|
||||
// #extension-usage-actor
|
||||
|
||||
public static class Connection {}
|
||||
|
||||
@Test
|
||||
public void demonstrateHowToCreateAndUseAnAkkaExtensionInJava() {
|
||||
final ActorSystem system = null;
|
||||
try {
|
||||
// #extension-usage
|
||||
// typically you would use static import of the Settings.SettingsProvider field
|
||||
String dbUri = Settings.SettingsProvider.get(system).DB_URI;
|
||||
// #extension-usage
|
||||
} catch (Exception e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
21
docs/src/test/java/jdocs/future/ActorWithFuture.java
Normal file
21
docs/src/test/java/jdocs/future/ActorWithFuture.java
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.future;
|
||||
|
||||
// #context-dispatcher
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.dispatch.Futures;
|
||||
|
||||
public class ActorWithFuture extends AbstractActor {
|
||||
ActorWithFuture() {
|
||||
Futures.future(() -> "hello", getContext().dispatcher());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return AbstractActor.emptyBehavior();
|
||||
}
|
||||
}
|
||||
// #context-dispatcher
|
||||
80
docs/src/test/java/jdocs/future/FutureDocTest.java
Normal file
80
docs/src/test/java/jdocs/future/FutureDocTest.java
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.future;
|
||||
|
||||
import org.apache.pekko.actor.typed.ActorSystem;
|
||||
import org.apache.pekko.dispatch.Futures;
|
||||
import org.apache.pekko.pattern.Patterns;
|
||||
import org.apache.pekko.testkit.AkkaJUnitActorSystemResource;
|
||||
import org.apache.pekko.testkit.AkkaSpec;
|
||||
import org.apache.pekko.util.Timeout;
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import scala.compat.java8.FutureConverters;
|
||||
import scala.concurrent.Await;
|
||||
import scala.concurrent.ExecutionContext;
|
||||
import scala.concurrent.Future;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
import static org.apache.pekko.actor.typed.javadsl.Adapter.toTyped;
|
||||
import static org.apache.pekko.dispatch.Futures.future;
|
||||
// #imports
|
||||
import org.apache.pekko.pattern.Patterns;
|
||||
|
||||
// #imports
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
public class FutureDocTest extends AbstractJavaTest {
|
||||
|
||||
@ClassRule
|
||||
public static AkkaJUnitActorSystemResource actorSystemResource =
|
||||
new AkkaJUnitActorSystemResource("FutureDocTest", AkkaSpec.testConf());
|
||||
|
||||
private final ActorSystem<Void> system = toTyped(actorSystemResource.getSystem());
|
||||
|
||||
@Test(expected = java.util.concurrent.CompletionException.class)
|
||||
public void useAfter() throws Exception {
|
||||
final ExecutionContext ec = system.executionContext();
|
||||
// #after
|
||||
CompletionStage<String> failWithException =
|
||||
CompletableFuture.supplyAsync(
|
||||
() -> {
|
||||
throw new IllegalStateException("OHNOES1");
|
||||
});
|
||||
CompletionStage<String> delayed =
|
||||
Patterns.after(Duration.ofMillis(200), system, () -> failWithException);
|
||||
// #after
|
||||
Future<String> future =
|
||||
future(
|
||||
() -> {
|
||||
Thread.sleep(1000);
|
||||
return "foo";
|
||||
},
|
||||
ec);
|
||||
Future<String> result =
|
||||
Futures.firstCompletedOf(
|
||||
Arrays.<Future<String>>asList(future, FutureConverters.toScala(delayed)), ec);
|
||||
Timeout timeout = Timeout.create(Duration.ofSeconds(2));
|
||||
Await.result(result, timeout.duration());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void useRetry() throws Exception {
|
||||
// #retry
|
||||
Callable<CompletionStage<String>> attempt = () -> CompletableFuture.completedFuture("test");
|
||||
|
||||
CompletionStage<String> retriedFuture =
|
||||
Patterns.retry(attempt, 3, java.time.Duration.ofMillis(200), system);
|
||||
// #retry
|
||||
|
||||
retriedFuture.toCompletableFuture().get(2, SECONDS);
|
||||
}
|
||||
}
|
||||
98
docs/src/test/java/jdocs/io/IODocTest.java
Normal file
98
docs/src/test/java/jdocs/io/IODocTest.java
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.io;
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
// #imports
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.io.Inet;
|
||||
import org.apache.pekko.io.Tcp;
|
||||
import org.apache.pekko.io.TcpMessage;
|
||||
import org.apache.pekko.io.TcpSO;
|
||||
import org.apache.pekko.util.ByteString;
|
||||
import java.time.Duration;
|
||||
// #imports
|
||||
|
||||
public class IODocTest {
|
||||
|
||||
public static class Demo extends AbstractActor {
|
||||
ActorRef connectionActor = null;
|
||||
ActorRef listener = getSelf();
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.matchEquals(
|
||||
"connect",
|
||||
msg -> {
|
||||
// #manager
|
||||
final ActorRef tcp = Tcp.get(system).manager();
|
||||
// #manager
|
||||
// #connect
|
||||
final InetSocketAddress remoteAddr = new InetSocketAddress("127.0.0.1", 12345);
|
||||
tcp.tell(TcpMessage.connect(remoteAddr), getSelf());
|
||||
// #connect
|
||||
// #connect-with-options
|
||||
final InetSocketAddress localAddr = new InetSocketAddress("127.0.0.1", 1234);
|
||||
final List<Inet.SocketOption> options = new ArrayList<Inet.SocketOption>();
|
||||
options.add(TcpSO.keepAlive(true));
|
||||
Duration timeout = null;
|
||||
tcp.tell(
|
||||
TcpMessage.connect(remoteAddr, localAddr, options, timeout, false), getSelf());
|
||||
// #connect-with-options
|
||||
})
|
||||
// #connected
|
||||
.match(
|
||||
Tcp.Connected.class,
|
||||
conn -> {
|
||||
connectionActor = getSender();
|
||||
connectionActor.tell(TcpMessage.register(listener), getSelf());
|
||||
})
|
||||
// #connected
|
||||
// #received
|
||||
.match(
|
||||
Tcp.Received.class,
|
||||
recv -> {
|
||||
final ByteString data = recv.data();
|
||||
// and do something with the received data ...
|
||||
})
|
||||
.match(
|
||||
Tcp.CommandFailed.class,
|
||||
failed -> {
|
||||
final Tcp.Command command = failed.cmd();
|
||||
// react to failed connect, bind, write, etc.
|
||||
})
|
||||
.match(
|
||||
Tcp.ConnectionClosed.class,
|
||||
closed -> {
|
||||
if (closed.isAborted()) {
|
||||
// handle close reasons like this
|
||||
}
|
||||
})
|
||||
// #received
|
||||
.matchEquals(
|
||||
"bind",
|
||||
msg -> {
|
||||
final ActorRef handler = getSelf();
|
||||
// #bind
|
||||
final ActorRef tcp = Tcp.get(system).manager();
|
||||
final InetSocketAddress localAddr = new InetSocketAddress("127.0.0.1", 1234);
|
||||
final List<Inet.SocketOption> options = new ArrayList<Inet.SocketOption>();
|
||||
options.add(TcpSO.reuseAddress(true));
|
||||
tcp.tell(TcpMessage.bind(handler, localAddr, 10, options, false), getSelf());
|
||||
// #bind
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
static ActorSystem system;
|
||||
|
||||
// This is currently only a compilation test, nothing is run
|
||||
}
|
||||
106
docs/src/test/java/jdocs/io/JavaReadBackPressure.java
Normal file
106
docs/src/test/java/jdocs/io/JavaReadBackPressure.java
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.io;
|
||||
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.io.Inet;
|
||||
import org.apache.pekko.io.Tcp;
|
||||
import org.apache.pekko.io.TcpMessage;
|
||||
import org.apache.pekko.util.ByteString;
|
||||
import java.time.Duration;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com> */
|
||||
public class JavaReadBackPressure {
|
||||
|
||||
public static class Listener extends AbstractActor {
|
||||
ActorRef tcp;
|
||||
ActorRef listener;
|
||||
|
||||
@Override
|
||||
// #pull-accepting
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Tcp.Bound.class,
|
||||
x -> {
|
||||
listener = getSender();
|
||||
// Accept connections one by one
|
||||
listener.tell(TcpMessage.resumeAccepting(1), getSelf());
|
||||
})
|
||||
.match(
|
||||
Tcp.Connected.class,
|
||||
x -> {
|
||||
ActorRef handler = getContext().actorOf(Props.create(PullEcho.class, getSender()));
|
||||
getSender().tell(TcpMessage.register(handler), getSelf());
|
||||
// Resume accepting connections
|
||||
listener.tell(TcpMessage.resumeAccepting(1), getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
// #pull-accepting
|
||||
|
||||
@Override
|
||||
public void preStart() throws Exception {
|
||||
// #pull-mode-bind
|
||||
tcp = Tcp.get(getContext().getSystem()).manager();
|
||||
final List<Inet.SocketOption> options = new ArrayList<Inet.SocketOption>();
|
||||
tcp.tell(
|
||||
TcpMessage.bind(getSelf(), new InetSocketAddress("localhost", 0), 100, options, true),
|
||||
getSelf());
|
||||
// #pull-mode-bind
|
||||
}
|
||||
|
||||
private void demonstrateConnect() {
|
||||
// #pull-mode-connect
|
||||
final List<Inet.SocketOption> options = new ArrayList<Inet.SocketOption>();
|
||||
Duration timeout = null;
|
||||
tcp.tell(
|
||||
TcpMessage.connect(
|
||||
new InetSocketAddress("localhost", 3000), null, options, timeout, true),
|
||||
getSelf());
|
||||
// #pull-mode-connect
|
||||
}
|
||||
}
|
||||
|
||||
public static class Ack implements Tcp.Event {}
|
||||
|
||||
public static class PullEcho extends AbstractActor {
|
||||
final ActorRef connection;
|
||||
|
||||
public PullEcho(ActorRef connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
// #pull-reading-echo
|
||||
@Override
|
||||
public void preStart() throws Exception {
|
||||
connection.tell(TcpMessage.resumeReading(), getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Tcp.Received.class,
|
||||
message -> {
|
||||
ByteString data = message.data();
|
||||
connection.tell(TcpMessage.write(data, new Ack()), getSelf());
|
||||
})
|
||||
.match(
|
||||
Ack.class,
|
||||
message -> {
|
||||
connection.tell(TcpMessage.resumeReading(), getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
// #pull-reading-echo
|
||||
}
|
||||
}
|
||||
135
docs/src/test/java/jdocs/io/JavaUdpMulticast.java
Normal file
135
docs/src/test/java/jdocs/io/JavaUdpMulticast.java
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.io;
|
||||
|
||||
// #imports
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.event.Logging;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
import org.apache.pekko.io.Inet;
|
||||
import org.apache.pekko.io.Udp;
|
||||
import org.apache.pekko.io.UdpMessage;
|
||||
import org.apache.pekko.util.ByteString;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.StandardProtocolFamily;
|
||||
import java.net.DatagramSocket;
|
||||
import java.nio.channels.DatagramChannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
// #imports
|
||||
|
||||
public class JavaUdpMulticast {
|
||||
// #inet6-protocol-family
|
||||
public static class Inet6ProtocolFamily extends Inet.DatagramChannelCreator {
|
||||
@Override
|
||||
public DatagramChannel create() throws Exception {
|
||||
return DatagramChannel.open(StandardProtocolFamily.INET6);
|
||||
}
|
||||
}
|
||||
// #inet6-protocol-family
|
||||
|
||||
// #multicast-group
|
||||
public static class MulticastGroup extends Inet.AbstractSocketOptionV2 {
|
||||
private String address;
|
||||
private String interf;
|
||||
|
||||
public MulticastGroup(String address, String interf) {
|
||||
this.address = address;
|
||||
this.interf = interf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterBind(DatagramSocket s) {
|
||||
try {
|
||||
InetAddress group = InetAddress.getByName(address);
|
||||
NetworkInterface networkInterface = NetworkInterface.getByName(interf);
|
||||
s.getChannel().join(group, networkInterface);
|
||||
} catch (Exception ex) {
|
||||
System.out.println("Unable to join multicast group.");
|
||||
}
|
||||
}
|
||||
}
|
||||
// #multicast-group
|
||||
|
||||
public static class Listener extends AbstractActor {
|
||||
LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
|
||||
|
||||
ActorRef sink;
|
||||
|
||||
public Listener(String iface, String group, Integer port, ActorRef sink) {
|
||||
this.sink = sink;
|
||||
|
||||
// #bind
|
||||
List<Inet.SocketOption> options = new ArrayList<>();
|
||||
options.add(new Inet6ProtocolFamily());
|
||||
options.add(new MulticastGroup(group, iface));
|
||||
|
||||
final ActorRef mgr = Udp.get(getContext().getSystem()).getManager();
|
||||
// listen for datagrams on this address
|
||||
InetSocketAddress endpoint = new InetSocketAddress(port);
|
||||
mgr.tell(UdpMessage.bind(getSelf(), endpoint, options), getSelf());
|
||||
// #bind
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Udp.Bound.class,
|
||||
bound -> {
|
||||
log.info("Bound to {}", bound.localAddress());
|
||||
sink.tell(bound, getSelf());
|
||||
})
|
||||
.match(
|
||||
Udp.Received.class,
|
||||
received -> {
|
||||
final String txt = received.data().decodeString("utf-8");
|
||||
log.info("Received '{}' from {}", txt, received.sender());
|
||||
sink.tell(txt, getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Sender extends AbstractActor {
|
||||
LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
|
||||
|
||||
String iface;
|
||||
String group;
|
||||
Integer port;
|
||||
String message;
|
||||
|
||||
public Sender(String iface, String group, Integer port, String msg) {
|
||||
this.iface = iface;
|
||||
this.group = group;
|
||||
this.port = port;
|
||||
this.message = msg;
|
||||
|
||||
List<Inet.SocketOption> options = new ArrayList<>();
|
||||
options.add(new Inet6ProtocolFamily());
|
||||
|
||||
final ActorRef mgr = Udp.get(getContext().getSystem()).getManager();
|
||||
mgr.tell(UdpMessage.simpleSender(options), getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Udp.SimpleSenderReady.class,
|
||||
x -> {
|
||||
InetSocketAddress remote = new InetSocketAddress(group + "%" + iface, port);
|
||||
log.info("Sending message to " + remote);
|
||||
getSender()
|
||||
.tell(UdpMessage.send(ByteString.fromString(message), remote), getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
116
docs/src/test/java/jdocs/io/JavaUdpMulticastTest.java
Normal file
116
docs/src/test/java/jdocs/io/JavaUdpMulticastTest.java
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.io;
|
||||
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.io.Udp;
|
||||
import org.apache.pekko.testkit.SocketUtil;
|
||||
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.util.*;
|
||||
|
||||
public class JavaUdpMulticastTest extends AbstractJavaTest {
|
||||
|
||||
static ActorSystem system;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
system = ActorSystem.create("JavaUdpMulticastTest");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUdpMulticast() throws Exception {
|
||||
new TestKit(system) {
|
||||
{
|
||||
List<NetworkInterface> ipv6Ifaces = new ArrayList<>();
|
||||
for (Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
||||
interfaces.hasMoreElements(); ) {
|
||||
NetworkInterface interf = interfaces.nextElement();
|
||||
if (interf.isUp() && interf.supportsMulticast()) {
|
||||
for (Enumeration<InetAddress> addresses = interf.getInetAddresses();
|
||||
addresses.hasMoreElements(); ) {
|
||||
InetAddress address = addresses.nextElement();
|
||||
if (address instanceof Inet6Address) {
|
||||
ipv6Ifaces.add(interf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ipv6Ifaces.isEmpty()) {
|
||||
system
|
||||
.log()
|
||||
.info(
|
||||
"JavaUdpMulticastTest skipped since no ipv6 interface supporting multicast could be found");
|
||||
} else {
|
||||
// lots of problems with choosing the wrong interface for this test depending
|
||||
// on the platform (awsdl0 can't be used on OSX, docker[0-9] can't be used in a docker
|
||||
// machine etc.)
|
||||
// therefore: try hard to find an interface that _does_ work, and only fail if there was
|
||||
// any potentially
|
||||
// working interfaces but all failed
|
||||
for (Iterator<NetworkInterface> interfaceIterator = ipv6Ifaces.iterator();
|
||||
interfaceIterator.hasNext(); ) {
|
||||
NetworkInterface ipv6Iface = interfaceIterator.next();
|
||||
// host assigned link local multicast address
|
||||
// https://www.rfc-editor.org/rfc/rfc3307#section-4.3.2
|
||||
// generate a random 32 bit multicast address with the high order bit set
|
||||
final String randomAddress =
|
||||
Long.toHexString(((long) Math.abs(new Random().nextInt())) | (1L << 31))
|
||||
.toUpperCase();
|
||||
final StringBuilder groupBuilder = new StringBuilder("FF02:");
|
||||
for (int i = 0; i < 2; i += 1) {
|
||||
groupBuilder.append(":");
|
||||
groupBuilder.append(randomAddress.subSequence(i * 4, i * 4 + 4));
|
||||
}
|
||||
final String group = groupBuilder.toString();
|
||||
final Integer port = SocketUtil.temporaryUdpIpv6Port(ipv6Iface);
|
||||
final String msg = "ohi";
|
||||
final ActorRef sink = getRef();
|
||||
final String iface = ipv6Iface.getName();
|
||||
|
||||
final ActorRef listener =
|
||||
system.actorOf(
|
||||
Props.create(JavaUdpMulticast.Listener.class, iface, group, port, sink));
|
||||
|
||||
try {
|
||||
expectMsgClass(Udp.Bound.class);
|
||||
final ActorRef sender =
|
||||
system.actorOf(
|
||||
Props.create(JavaUdpMulticast.Sender.class, iface, group, port, msg));
|
||||
expectMsgEquals(msg);
|
||||
// success with one interface is enough
|
||||
break;
|
||||
|
||||
} catch (AssertionError ex) {
|
||||
if (!interfaceIterator.hasNext()) throw ex;
|
||||
else {
|
||||
system.log().info("Failed to run test on interface {}", ipv6Iface.getDisplayName());
|
||||
}
|
||||
} finally {
|
||||
// unbind
|
||||
system.stop(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDown() {
|
||||
TestKit.shutdownActorSystem(system);
|
||||
system = null;
|
||||
}
|
||||
}
|
||||
96
docs/src/test/java/jdocs/io/UdpConnectedDocTest.java
Normal file
96
docs/src/test/java/jdocs/io/UdpConnectedDocTest.java
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.io;
|
||||
|
||||
import org.apache.pekko.japi.pf.ReceiveBuilder;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
// #imports
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.io.Inet;
|
||||
import org.apache.pekko.io.UdpConnected;
|
||||
import org.apache.pekko.io.UdpConnectedMessage;
|
||||
import org.apache.pekko.io.UdpSO;
|
||||
import org.apache.pekko.util.ByteString;
|
||||
|
||||
import static org.apache.pekko.util.ByteString.emptyByteString;
|
||||
|
||||
// #imports
|
||||
|
||||
public class UdpConnectedDocTest {
|
||||
|
||||
public static class Demo extends AbstractActor {
|
||||
ActorRef connectionActor = null;
|
||||
ActorRef handler = getSelf();
|
||||
ActorSystem system = getContext().getSystem();
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
ReceiveBuilder builder = receiveBuilder();
|
||||
builder.matchEquals(
|
||||
"connect",
|
||||
message -> {
|
||||
// #manager
|
||||
final ActorRef udp = UdpConnected.get(system).manager();
|
||||
// #manager
|
||||
// #connect
|
||||
final InetSocketAddress remoteAddr = new InetSocketAddress("127.0.0.1", 12345);
|
||||
udp.tell(UdpConnectedMessage.connect(handler, remoteAddr), getSelf());
|
||||
// #connect
|
||||
// #connect-with-options
|
||||
final InetSocketAddress localAddr = new InetSocketAddress("127.0.0.1", 1234);
|
||||
final List<Inet.SocketOption> options = new ArrayList<Inet.SocketOption>();
|
||||
options.add(UdpSO.broadcast(true));
|
||||
udp.tell(
|
||||
UdpConnectedMessage.connect(handler, remoteAddr, localAddr, options), getSelf());
|
||||
// #connect-with-options
|
||||
});
|
||||
// #connected
|
||||
builder.match(
|
||||
UdpConnected.Connected.class,
|
||||
conn -> {
|
||||
connectionActor = getSender(); // Save the worker ref for later use
|
||||
});
|
||||
// #connected
|
||||
// #received
|
||||
builder
|
||||
.match(
|
||||
UdpConnected.Received.class,
|
||||
recv -> {
|
||||
final ByteString data = recv.data();
|
||||
// and do something with the received data ...
|
||||
})
|
||||
.match(
|
||||
UdpConnected.CommandFailed.class,
|
||||
failed -> {
|
||||
final UdpConnected.Command command = failed.cmd();
|
||||
// react to failed connect, etc.
|
||||
})
|
||||
.match(
|
||||
UdpConnected.Disconnected.class,
|
||||
x -> {
|
||||
// do something on disconnect
|
||||
});
|
||||
// #received
|
||||
builder.matchEquals(
|
||||
"send",
|
||||
x -> {
|
||||
ByteString data = emptyByteString();
|
||||
// #send
|
||||
connectionActor.tell(UdpConnectedMessage.send(data), getSelf());
|
||||
// #send
|
||||
});
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void demonstrateConnect() {}
|
||||
}
|
||||
181
docs/src/test/java/jdocs/io/UdpDocTest.java
Normal file
181
docs/src/test/java/jdocs/io/UdpDocTest.java
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.io;
|
||||
|
||||
// #imports
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.PoisonPill;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.io.Udp;
|
||||
import org.apache.pekko.io.UdpConnected;
|
||||
import org.apache.pekko.io.UdpConnectedMessage;
|
||||
import org.apache.pekko.io.UdpMessage;
|
||||
import org.apache.pekko.util.ByteString;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
// #imports
|
||||
|
||||
public class UdpDocTest {
|
||||
|
||||
// #sender
|
||||
public static class SimpleSender extends AbstractActor {
|
||||
final InetSocketAddress remote;
|
||||
|
||||
public SimpleSender(InetSocketAddress remote) {
|
||||
this.remote = remote;
|
||||
|
||||
// request creation of a SimpleSender
|
||||
final ActorRef mgr = Udp.get(getContext().getSystem()).getManager();
|
||||
mgr.tell(UdpMessage.simpleSender(), getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Udp.SimpleSenderReady.class,
|
||||
message -> {
|
||||
getContext().become(ready(getSender()));
|
||||
// #sender
|
||||
getSender()
|
||||
.tell(UdpMessage.send(ByteString.fromString("hello"), remote), getSelf());
|
||||
// #sender
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private Receive ready(final ActorRef send) {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
message -> {
|
||||
send.tell(UdpMessage.send(ByteString.fromString(message), remote), getSelf());
|
||||
// #sender
|
||||
if (message.equals("world")) {
|
||||
send.tell(PoisonPill.getInstance(), getSelf());
|
||||
}
|
||||
// #sender
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #sender
|
||||
|
||||
// #listener
|
||||
public static class Listener extends AbstractActor {
|
||||
final ActorRef nextActor;
|
||||
|
||||
public Listener(ActorRef nextActor) {
|
||||
this.nextActor = nextActor;
|
||||
|
||||
// request creation of a bound listen socket
|
||||
final ActorRef mgr = Udp.get(getContext().getSystem()).getManager();
|
||||
mgr.tell(UdpMessage.bind(getSelf(), new InetSocketAddress("localhost", 0)), getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Udp.Bound.class,
|
||||
bound -> {
|
||||
// #listener
|
||||
nextActor.tell(bound.localAddress(), getSender());
|
||||
// #listener
|
||||
getContext().become(ready(getSender()));
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private Receive ready(final ActorRef socket) {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Udp.Received.class,
|
||||
r -> {
|
||||
// echo server example: send back the data
|
||||
socket.tell(UdpMessage.send(r.data(), r.sender()), getSelf());
|
||||
// or do some processing and forward it on
|
||||
final Object processed = // parse data etc., e.g. using PipelineStage
|
||||
// #listener
|
||||
r.data().utf8String();
|
||||
// #listener
|
||||
nextActor.tell(processed, getSelf());
|
||||
})
|
||||
.matchEquals(
|
||||
UdpMessage.unbind(),
|
||||
message -> {
|
||||
socket.tell(message, getSelf());
|
||||
})
|
||||
.match(
|
||||
Udp.Unbound.class,
|
||||
message -> {
|
||||
getContext().stop(getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #listener
|
||||
|
||||
// #connected
|
||||
public static class Connected extends AbstractActor {
|
||||
final InetSocketAddress remote;
|
||||
|
||||
public Connected(InetSocketAddress remote) {
|
||||
this.remote = remote;
|
||||
|
||||
// create a restricted a.k.a. “connected” socket
|
||||
final ActorRef mgr = UdpConnected.get(getContext().getSystem()).getManager();
|
||||
mgr.tell(UdpConnectedMessage.connect(getSelf(), remote), getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
UdpConnected.Connected.class,
|
||||
message -> {
|
||||
getContext().become(ready(getSender()));
|
||||
// #connected
|
||||
getSender()
|
||||
.tell(UdpConnectedMessage.send(ByteString.fromString("hello")), getSelf());
|
||||
// #connected
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private Receive ready(final ActorRef connection) {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
UdpConnected.Received.class,
|
||||
r -> {
|
||||
// process data, send it on, etc.
|
||||
// #connected
|
||||
if (r.data().utf8String().equals("hello")) {
|
||||
connection.tell(
|
||||
UdpConnectedMessage.send(ByteString.fromString("world")), getSelf());
|
||||
}
|
||||
// #connected
|
||||
})
|
||||
.match(
|
||||
String.class,
|
||||
str -> {
|
||||
connection.tell(UdpConnectedMessage.send(ByteString.fromString(str)), getSelf());
|
||||
})
|
||||
.matchEquals(
|
||||
UdpConnectedMessage.disconnect(),
|
||||
message -> {
|
||||
connection.tell(message, getSelf());
|
||||
})
|
||||
.match(
|
||||
UdpConnected.Disconnected.class,
|
||||
x -> {
|
||||
getContext().stop(getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #connected
|
||||
|
||||
}
|
||||
259
docs/src/test/java/jdocs/io/japi/EchoHandler.java
Normal file
259
docs/src/test/java/jdocs/io/japi/EchoHandler.java
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.io.japi;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.event.Logging;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
import org.apache.pekko.io.Tcp.CommandFailed;
|
||||
import org.apache.pekko.io.Tcp.ConnectionClosed;
|
||||
import org.apache.pekko.io.Tcp.Event;
|
||||
import org.apache.pekko.io.Tcp.Received;
|
||||
import org.apache.pekko.io.Tcp.Write;
|
||||
import org.apache.pekko.io.Tcp.WritingResumed;
|
||||
import org.apache.pekko.io.TcpMessage;
|
||||
import org.apache.pekko.util.ByteString;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
// #echo-handler
|
||||
public class EchoHandler extends AbstractActor {
|
||||
|
||||
final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), getSelf());
|
||||
|
||||
final ActorRef connection;
|
||||
final InetSocketAddress remote;
|
||||
|
||||
public static final long MAX_STORED = 100000000;
|
||||
public static final long HIGH_WATERMARK = MAX_STORED * 5 / 10;
|
||||
public static final long LOW_WATERMARK = MAX_STORED * 2 / 10;
|
||||
|
||||
private long transferred;
|
||||
private int storageOffset = 0;
|
||||
private long stored = 0;
|
||||
private Queue<ByteString> storage = new LinkedList<>();
|
||||
|
||||
private boolean suspended = false;
|
||||
|
||||
private static class Ack implements Event {
|
||||
public final int ack;
|
||||
|
||||
public Ack(int ack) {
|
||||
this.ack = ack;
|
||||
}
|
||||
}
|
||||
|
||||
public EchoHandler(ActorRef connection, InetSocketAddress remote) {
|
||||
this.connection = connection;
|
||||
this.remote = remote;
|
||||
|
||||
writing = writing();
|
||||
|
||||
// sign death pact: this actor stops when the connection is closed
|
||||
getContext().watch(connection);
|
||||
|
||||
// start out in optimistic write-through mode
|
||||
getContext().become(writing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return writing;
|
||||
}
|
||||
|
||||
private final Receive writing;
|
||||
|
||||
private Receive writing() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Received.class,
|
||||
msg -> {
|
||||
final ByteString data = msg.data();
|
||||
connection.tell(TcpMessage.write(data, new Ack(currentOffset())), getSelf());
|
||||
buffer(data);
|
||||
})
|
||||
.match(
|
||||
Integer.class,
|
||||
msg -> {
|
||||
acknowledge(msg);
|
||||
})
|
||||
.match(
|
||||
CommandFailed.class,
|
||||
msg -> {
|
||||
final Write w = (Write) msg.cmd();
|
||||
connection.tell(TcpMessage.resumeWriting(), getSelf());
|
||||
getContext().become(buffering((Ack) w.ack()));
|
||||
})
|
||||
.match(
|
||||
ConnectionClosed.class,
|
||||
msg -> {
|
||||
if (msg.isPeerClosed()) {
|
||||
if (storage.isEmpty()) {
|
||||
getContext().stop(getSelf());
|
||||
} else {
|
||||
getContext().become(closing());
|
||||
}
|
||||
}
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
// #buffering
|
||||
|
||||
static final class BufferingState {
|
||||
int toAck = 10;
|
||||
boolean peerClosed = false;
|
||||
}
|
||||
|
||||
protected Receive buffering(final Ack nack) {
|
||||
final BufferingState state = new BufferingState();
|
||||
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Received.class,
|
||||
msg -> {
|
||||
buffer(msg.data());
|
||||
})
|
||||
.match(
|
||||
WritingResumed.class,
|
||||
msg -> {
|
||||
writeFirst();
|
||||
})
|
||||
.match(
|
||||
ConnectionClosed.class,
|
||||
msg -> {
|
||||
if (msg.isPeerClosed()) state.peerClosed = true;
|
||||
else getContext().stop(getSelf());
|
||||
})
|
||||
.match(
|
||||
Integer.class,
|
||||
ack -> {
|
||||
acknowledge(ack);
|
||||
|
||||
if (ack >= nack.ack) {
|
||||
// otherwise it was the ack of the last successful write
|
||||
|
||||
if (storage.isEmpty()) {
|
||||
if (state.peerClosed) getContext().stop(getSelf());
|
||||
else getContext().become(writing);
|
||||
|
||||
} else {
|
||||
if (state.toAck > 0) {
|
||||
// stay in ACK-based mode for a short while
|
||||
writeFirst();
|
||||
--state.toAck;
|
||||
} else {
|
||||
// then return to NACK-based again
|
||||
writeAll();
|
||||
if (state.peerClosed) getContext().become(closing());
|
||||
else getContext().become(writing);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.build();
|
||||
}
|
||||
// #buffering
|
||||
|
||||
// #closing
|
||||
protected Receive closing() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
CommandFailed.class,
|
||||
msg -> {
|
||||
// the command can only have been a Write
|
||||
connection.tell(TcpMessage.resumeWriting(), getSelf());
|
||||
getContext().become(closeResend(), false);
|
||||
})
|
||||
.match(
|
||||
Integer.class,
|
||||
msg -> {
|
||||
acknowledge(msg);
|
||||
if (storage.isEmpty()) getContext().stop(getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
protected Receive closeResend() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
WritingResumed.class,
|
||||
msg -> {
|
||||
writeAll();
|
||||
getContext().unbecome();
|
||||
})
|
||||
.match(
|
||||
Integer.class,
|
||||
msg -> {
|
||||
acknowledge(msg);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
// #closing
|
||||
|
||||
// #storage-omitted
|
||||
|
||||
@Override
|
||||
public void postStop() {
|
||||
log.info("transferred {} bytes from/to [{}]", transferred, remote);
|
||||
}
|
||||
|
||||
// #helpers
|
||||
protected void buffer(ByteString data) {
|
||||
storage.add(data);
|
||||
stored += data.size();
|
||||
|
||||
if (stored > MAX_STORED) {
|
||||
log.warning("drop connection to [{}] (buffer overrun)", remote);
|
||||
getContext().stop(getSelf());
|
||||
|
||||
} else if (stored > HIGH_WATERMARK) {
|
||||
log.debug("suspending reading at {}", currentOffset());
|
||||
connection.tell(TcpMessage.suspendReading(), getSelf());
|
||||
suspended = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected void acknowledge(int ack) {
|
||||
assertEquals(storageOffset, ack);
|
||||
assertFalse(storage.isEmpty());
|
||||
|
||||
final ByteString acked = storage.remove();
|
||||
stored -= acked.size();
|
||||
transferred += acked.size();
|
||||
storageOffset += 1;
|
||||
|
||||
if (suspended && stored < LOW_WATERMARK) {
|
||||
log.debug("resuming reading");
|
||||
connection.tell(TcpMessage.resumeReading(), getSelf());
|
||||
suspended = false;
|
||||
}
|
||||
}
|
||||
// #helpers
|
||||
|
||||
protected int currentOffset() {
|
||||
return storageOffset + storage.size();
|
||||
}
|
||||
|
||||
protected void writeAll() {
|
||||
int i = 0;
|
||||
for (ByteString data : storage) {
|
||||
connection.tell(TcpMessage.write(data, new Ack(storageOffset + i++)), getSelf());
|
||||
}
|
||||
}
|
||||
|
||||
protected void writeFirst() {
|
||||
connection.tell(TcpMessage.write(storage.peek(), new Ack(storageOffset)), getSelf());
|
||||
}
|
||||
|
||||
// #storage-omitted
|
||||
}
|
||||
// #echo-handler
|
||||
87
docs/src/test/java/jdocs/io/japi/EchoManager.java
Normal file
87
docs/src/test/java/jdocs/io/japi/EchoManager.java
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.io.japi;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.actor.SupervisorStrategy;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.event.Logging;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
import org.apache.pekko.io.Tcp;
|
||||
import org.apache.pekko.io.Tcp.Bind;
|
||||
import org.apache.pekko.io.Tcp.Bound;
|
||||
import org.apache.pekko.io.Tcp.Connected;
|
||||
import org.apache.pekko.io.TcpMessage;
|
||||
|
||||
public class EchoManager extends AbstractActor {
|
||||
|
||||
final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), getSelf());
|
||||
|
||||
final Class<?> handlerClass;
|
||||
|
||||
public EchoManager(Class<?> handlerClass) {
|
||||
this.handlerClass = handlerClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SupervisorStrategy supervisorStrategy() {
|
||||
return SupervisorStrategy.stoppingStrategy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preStart() throws Exception {
|
||||
// #manager
|
||||
final ActorRef tcpManager = Tcp.get(getContext().getSystem()).manager();
|
||||
// #manager
|
||||
tcpManager.tell(
|
||||
TcpMessage.bind(getSelf(), new InetSocketAddress("localhost", 0), 100), getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postRestart(Throwable arg0) throws Exception {
|
||||
// do not restart
|
||||
getContext().stop(getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Bound.class,
|
||||
msg -> {
|
||||
log.info("listening on [{}]", msg.localAddress());
|
||||
})
|
||||
.match(
|
||||
Tcp.CommandFailed.class,
|
||||
failed -> {
|
||||
if (failed.cmd() instanceof Bind) {
|
||||
log.warning("cannot bind to [{}]", ((Bind) failed.cmd()).localAddress());
|
||||
getContext().stop(getSelf());
|
||||
} else {
|
||||
log.warning("unknown command failed [{}]", failed.cmd());
|
||||
}
|
||||
})
|
||||
.match(
|
||||
Connected.class,
|
||||
conn -> {
|
||||
log.info("received connection from [{}]", conn.remoteAddress());
|
||||
final ActorRef connection = getSender();
|
||||
final ActorRef handler =
|
||||
getContext()
|
||||
.actorOf(Props.create(handlerClass, connection, conn.remoteAddress()));
|
||||
// #echo-manager
|
||||
connection.tell(
|
||||
TcpMessage.register(
|
||||
handler, true, // <-- keepOpenOnPeerClosed flag
|
||||
true),
|
||||
getSelf());
|
||||
// #echo-manager
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
36
docs/src/test/java/jdocs/io/japi/EchoServer.java
Normal file
36
docs/src/test/java/jdocs/io/japi/EchoServer.java
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.io.japi;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.Props;
|
||||
|
||||
import com.typesafe.config.Config;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
|
||||
public class EchoServer {
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
final Config config = ConfigFactory.parseString("akka.loglevel=DEBUG");
|
||||
final ActorSystem system = ActorSystem.create("EchoServer", config);
|
||||
try {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
final ActorRef watcher = system.actorOf(Props.create(Watcher.class, latch), "watcher");
|
||||
final ActorRef nackServer =
|
||||
system.actorOf(Props.create(EchoManager.class, EchoHandler.class), "nack");
|
||||
final ActorRef ackServer =
|
||||
system.actorOf(Props.create(EchoManager.class, SimpleEchoHandler.class), "ack");
|
||||
watcher.tell(nackServer, ActorRef.noSender());
|
||||
watcher.tell(ackServer, ActorRef.noSender());
|
||||
latch.await(10, TimeUnit.MINUTES);
|
||||
} finally {
|
||||
system.terminate();
|
||||
}
|
||||
}
|
||||
}
|
||||
205
docs/src/test/java/jdocs/io/japi/IODocTest.java
Normal file
205
docs/src/test/java/jdocs/io/japi/IODocTest.java
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.io.japi;
|
||||
|
||||
import org.apache.pekko.testkit.AkkaJUnitActorSystemResource;
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.apache.pekko.testkit.javadsl.TestKit;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
// #imports
|
||||
import java.net.InetSocketAddress;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.io.Tcp;
|
||||
import org.apache.pekko.io.Tcp.Bound;
|
||||
import org.apache.pekko.io.Tcp.CommandFailed;
|
||||
import org.apache.pekko.io.Tcp.Connected;
|
||||
import org.apache.pekko.io.Tcp.ConnectionClosed;
|
||||
import org.apache.pekko.io.Tcp.Received;
|
||||
import org.apache.pekko.io.TcpMessage;
|
||||
import org.apache.pekko.util.ByteString;
|
||||
// #imports
|
||||
|
||||
import org.apache.pekko.testkit.AkkaSpec;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IODocTest extends AbstractJavaTest {
|
||||
|
||||
public
|
||||
// #server
|
||||
static class Server extends AbstractActor {
|
||||
|
||||
final ActorRef manager;
|
||||
|
||||
public Server(ActorRef manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
public static Props props(ActorRef manager) {
|
||||
return Props.create(Server.class, manager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preStart() throws Exception {
|
||||
final ActorRef tcp = Tcp.get(getContext().getSystem()).manager();
|
||||
tcp.tell(TcpMessage.bind(getSelf(), new InetSocketAddress("localhost", 0), 100), getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Bound.class,
|
||||
msg -> {
|
||||
manager.tell(msg, getSelf());
|
||||
})
|
||||
.match(
|
||||
CommandFailed.class,
|
||||
msg -> {
|
||||
getContext().stop(getSelf());
|
||||
})
|
||||
.match(
|
||||
Connected.class,
|
||||
conn -> {
|
||||
manager.tell(conn, getSelf());
|
||||
final ActorRef handler =
|
||||
getContext().actorOf(Props.create(SimplisticHandler.class));
|
||||
getSender().tell(TcpMessage.register(handler), getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #server
|
||||
|
||||
public
|
||||
// #simplistic-handler
|
||||
static class SimplisticHandler extends AbstractActor {
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Received.class,
|
||||
msg -> {
|
||||
final ByteString data = msg.data();
|
||||
System.out.println(data);
|
||||
getSender().tell(TcpMessage.write(data), getSelf());
|
||||
})
|
||||
.match(
|
||||
ConnectionClosed.class,
|
||||
msg -> {
|
||||
getContext().stop(getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #simplistic-handler
|
||||
|
||||
public
|
||||
// #client
|
||||
static class Client extends AbstractActor {
|
||||
|
||||
final InetSocketAddress remote;
|
||||
final ActorRef listener;
|
||||
|
||||
public static Props props(InetSocketAddress remote, ActorRef listener) {
|
||||
return Props.create(Client.class, remote, listener);
|
||||
}
|
||||
|
||||
public Client(InetSocketAddress remote, ActorRef listener) {
|
||||
this.remote = remote;
|
||||
this.listener = listener;
|
||||
|
||||
final ActorRef tcp = Tcp.get(getContext().getSystem()).manager();
|
||||
tcp.tell(TcpMessage.connect(remote), getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
CommandFailed.class,
|
||||
msg -> {
|
||||
listener.tell("failed", getSelf());
|
||||
getContext().stop(getSelf());
|
||||
})
|
||||
.match(
|
||||
Connected.class,
|
||||
msg -> {
|
||||
listener.tell(msg, getSelf());
|
||||
getSender().tell(TcpMessage.register(getSelf()), getSelf());
|
||||
getContext().become(connected(getSender()));
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private Receive connected(final ActorRef connection) {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
ByteString.class,
|
||||
msg -> {
|
||||
connection.tell(TcpMessage.write((ByteString) msg), getSelf());
|
||||
})
|
||||
.match(
|
||||
CommandFailed.class,
|
||||
msg -> {
|
||||
// OS kernel socket buffer was full
|
||||
})
|
||||
.match(
|
||||
Received.class,
|
||||
msg -> {
|
||||
listener.tell(msg.data(), getSelf());
|
||||
})
|
||||
.matchEquals(
|
||||
"close",
|
||||
msg -> {
|
||||
connection.tell(TcpMessage.close(), getSelf());
|
||||
})
|
||||
.match(
|
||||
ConnectionClosed.class,
|
||||
msg -> {
|
||||
getContext().stop(getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #client
|
||||
|
||||
@ClassRule
|
||||
public static AkkaJUnitActorSystemResource actorSystemResource =
|
||||
new AkkaJUnitActorSystemResource("IODocTest", AkkaSpec.testConf());
|
||||
|
||||
private final ActorSystem system = actorSystemResource.getSystem();
|
||||
|
||||
@Test
|
||||
public void testConnection() {
|
||||
new TestKit(system) {
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
final ActorRef server = system.actorOf(Server.props(getRef()), "server1");
|
||||
final InetSocketAddress listen = expectMsgClass(Bound.class).localAddress();
|
||||
final ActorRef client = system.actorOf(Client.props(listen, getRef()), "client1");
|
||||
|
||||
final Connected c1 = expectMsgClass(Connected.class);
|
||||
final Connected c2 = expectMsgClass(Connected.class);
|
||||
assertTrue(c1.localAddress().equals(c2.remoteAddress()));
|
||||
assertTrue(c2.localAddress().equals(c1.remoteAddress()));
|
||||
|
||||
client.tell(ByteString.fromString("hello"), getRef());
|
||||
final ByteString reply = expectMsgClass(ByteString.class);
|
||||
assertEquals("hello", reply.utf8String());
|
||||
|
||||
watch(client);
|
||||
client.tell("close", getRef());
|
||||
expectTerminated(client);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
44
docs/src/test/java/jdocs/io/japi/Message.java
Normal file
44
docs/src/test/java/jdocs/io/japi/Message.java
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (C) 2013-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.io.japi;
|
||||
|
||||
// #message
|
||||
public class Message {
|
||||
|
||||
public static class Person {
|
||||
private final String first;
|
||||
private final String last;
|
||||
|
||||
public Person(String first, String last) {
|
||||
this.first = first;
|
||||
this.last = last;
|
||||
}
|
||||
|
||||
public String getFirst() {
|
||||
return first;
|
||||
}
|
||||
|
||||
public String getLast() {
|
||||
return last;
|
||||
}
|
||||
}
|
||||
|
||||
private final Person[] persons;
|
||||
private final double[] happinessCurve;
|
||||
|
||||
public Message(Person[] persons, double[] happinessCurve) {
|
||||
this.persons = persons;
|
||||
this.happinessCurve = happinessCurve;
|
||||
}
|
||||
|
||||
public Person[] getPersons() {
|
||||
return persons;
|
||||
}
|
||||
|
||||
public double[] getHappinessCurve() {
|
||||
return happinessCurve;
|
||||
}
|
||||
}
|
||||
// #message
|
||||
141
docs/src/test/java/jdocs/io/japi/SimpleEchoHandler.java
Normal file
141
docs/src/test/java/jdocs/io/japi/SimpleEchoHandler.java
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.io.japi;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.event.Logging;
|
||||
import org.apache.pekko.event.LoggingAdapter;
|
||||
import org.apache.pekko.io.Tcp.ConnectionClosed;
|
||||
import org.apache.pekko.io.Tcp.Event;
|
||||
import org.apache.pekko.io.Tcp.Received;
|
||||
import org.apache.pekko.io.TcpMessage;
|
||||
import org.apache.pekko.util.ByteString;
|
||||
|
||||
// #simple-echo-handler
|
||||
public class SimpleEchoHandler extends AbstractActor {
|
||||
|
||||
final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), getSelf());
|
||||
|
||||
final ActorRef connection;
|
||||
final InetSocketAddress remote;
|
||||
|
||||
public static final long maxStored = 100000000;
|
||||
public static final long highWatermark = maxStored * 5 / 10;
|
||||
public static final long lowWatermark = maxStored * 2 / 10;
|
||||
|
||||
public SimpleEchoHandler(ActorRef connection, InetSocketAddress remote) {
|
||||
this.connection = connection;
|
||||
this.remote = remote;
|
||||
|
||||
// sign death pact: this actor stops when the connection is closed
|
||||
getContext().watch(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Received.class,
|
||||
msg -> {
|
||||
final ByteString data = msg.data();
|
||||
buffer(data);
|
||||
connection.tell(TcpMessage.write(data, ACK), getSelf());
|
||||
// now switch behavior to “waiting for acknowledgement”
|
||||
getContext().become(buffering(), false);
|
||||
})
|
||||
.match(
|
||||
ConnectionClosed.class,
|
||||
msg -> {
|
||||
getContext().stop(getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private Receive buffering() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Received.class,
|
||||
msg -> {
|
||||
buffer(msg.data());
|
||||
})
|
||||
.match(
|
||||
Event.class,
|
||||
msg -> msg == ACK,
|
||||
msg -> {
|
||||
acknowledge();
|
||||
})
|
||||
.match(
|
||||
ConnectionClosed.class,
|
||||
msg -> {
|
||||
if (msg.isPeerClosed()) {
|
||||
closing = true;
|
||||
} else {
|
||||
// could also be ErrorClosed, in which case we just give up
|
||||
getContext().stop(getSelf());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
// #storage-omitted
|
||||
public void postStop() {
|
||||
log.info("transferred {} bytes from/to [{}]", transferred, remote);
|
||||
}
|
||||
|
||||
private long transferred;
|
||||
private long stored = 0;
|
||||
private Queue<ByteString> storage = new LinkedList<>();
|
||||
|
||||
private boolean suspended = false;
|
||||
private boolean closing = false;
|
||||
|
||||
private final Event ACK = new Event() {};
|
||||
|
||||
// #simple-helpers
|
||||
protected void buffer(ByteString data) {
|
||||
storage.add(data);
|
||||
stored += data.size();
|
||||
|
||||
if (stored > maxStored) {
|
||||
log.warning("drop connection to [{}] (buffer overrun)", remote);
|
||||
getContext().stop(getSelf());
|
||||
|
||||
} else if (stored > highWatermark) {
|
||||
log.debug("suspending reading");
|
||||
connection.tell(TcpMessage.suspendReading(), getSelf());
|
||||
suspended = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected void acknowledge() {
|
||||
final ByteString acked = storage.remove();
|
||||
stored -= acked.size();
|
||||
transferred += acked.size();
|
||||
|
||||
if (suspended && stored < lowWatermark) {
|
||||
log.debug("resuming reading");
|
||||
connection.tell(TcpMessage.resumeReading(), getSelf());
|
||||
suspended = false;
|
||||
}
|
||||
|
||||
if (storage.isEmpty()) {
|
||||
if (closing) {
|
||||
getContext().stop(getSelf());
|
||||
} else {
|
||||
getContext().unbecome();
|
||||
}
|
||||
} else {
|
||||
connection.tell(TcpMessage.write(storage.peek(), ACK), getSelf());
|
||||
}
|
||||
}
|
||||
// #simple-helpers
|
||||
// #storage-omitted
|
||||
}
|
||||
// #simple-echo-handler
|
||||
45
docs/src/test/java/jdocs/io/japi/Watcher.java
Normal file
45
docs/src/test/java/jdocs/io/japi/Watcher.java
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.io.japi;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.Terminated;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
|
||||
public class Watcher extends AbstractActor {
|
||||
|
||||
public static class Watch {
|
||||
final ActorRef target;
|
||||
|
||||
public Watch(ActorRef target) {
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
final CountDownLatch latch;
|
||||
|
||||
public Watcher(CountDownLatch latch) {
|
||||
this.latch = latch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Watch.class,
|
||||
msg -> {
|
||||
getContext().watch(msg.target);
|
||||
})
|
||||
.match(
|
||||
Terminated.class,
|
||||
msg -> {
|
||||
latch.countDown();
|
||||
if (latch.getCount() == 0) getContext().stop(getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.pattern;
|
||||
|
||||
import org.apache.pekko.actor.*;
|
||||
import org.apache.pekko.pattern.BackoffOpts;
|
||||
import org.apache.pekko.pattern.BackoffSupervisor;
|
||||
import org.apache.pekko.testkit.TestActors.EchoActor;
|
||||
// #backoff-imports
|
||||
import java.time.Duration;
|
||||
// #backoff-imports
|
||||
|
||||
public class BackoffSupervisorDocTest {
|
||||
|
||||
void exampleStop(ActorSystem system) {
|
||||
// #backoff-stop
|
||||
final Props childProps = Props.create(EchoActor.class);
|
||||
|
||||
final Props supervisorProps =
|
||||
BackoffSupervisor.props(
|
||||
BackoffOpts.onStop(
|
||||
childProps,
|
||||
"myEcho",
|
||||
Duration.ofSeconds(3),
|
||||
Duration.ofSeconds(30),
|
||||
0.2)); // adds 20% "noise" to vary the intervals slightly
|
||||
|
||||
system.actorOf(supervisorProps, "echoSupervisor");
|
||||
// #backoff-stop
|
||||
}
|
||||
|
||||
void exampleFailure(ActorSystem system) {
|
||||
// #backoff-fail
|
||||
final Props childProps = Props.create(EchoActor.class);
|
||||
|
||||
final Props supervisorProps =
|
||||
BackoffSupervisor.props(
|
||||
BackoffOpts.onFailure(
|
||||
childProps,
|
||||
"myEcho",
|
||||
Duration.ofSeconds(3),
|
||||
Duration.ofSeconds(30),
|
||||
0.2)); // adds 20% "noise" to vary the intervals slightly
|
||||
|
||||
system.actorOf(supervisorProps, "echoSupervisor");
|
||||
// #backoff-fail
|
||||
}
|
||||
}
|
||||
122
docs/src/test/java/jdocs/pattern/SupervisedAsk.java
Normal file
122
docs/src/test/java/jdocs/pattern/SupervisedAsk.java
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.pattern;
|
||||
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.apache.pekko.actor.ActorKilledException;
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorRefFactory;
|
||||
import org.apache.pekko.actor.Cancellable;
|
||||
import org.apache.pekko.actor.OneForOneStrategy;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.actor.Scheduler;
|
||||
import org.apache.pekko.actor.Status;
|
||||
import org.apache.pekko.actor.SupervisorStrategy;
|
||||
import org.apache.pekko.actor.Terminated;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
import org.apache.pekko.pattern.Patterns;
|
||||
|
||||
public class SupervisedAsk {
|
||||
|
||||
private static class AskParam {
|
||||
Props props;
|
||||
Object message;
|
||||
Duration timeout;
|
||||
|
||||
AskParam(Props props, Object message, Duration timeout) {
|
||||
this.props = props;
|
||||
this.message = message;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
}
|
||||
|
||||
private static class AskTimeout {}
|
||||
|
||||
public static class AskSupervisorCreator extends AbstractActor {
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
AskParam.class,
|
||||
message -> {
|
||||
ActorRef supervisor = getContext().actorOf(Props.create(AskSupervisor.class));
|
||||
supervisor.forward(message, getContext());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class AskSupervisor extends AbstractActor {
|
||||
private ActorRef targetActor;
|
||||
private ActorRef caller;
|
||||
private AskParam askParam;
|
||||
private Cancellable timeoutMessage;
|
||||
|
||||
@Override
|
||||
public SupervisorStrategy supervisorStrategy() {
|
||||
return new OneForOneStrategy(
|
||||
0,
|
||||
Duration.ZERO,
|
||||
cause -> {
|
||||
caller.tell(new Status.Failure(cause), getSelf());
|
||||
return SupervisorStrategy.stop();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
AskParam.class,
|
||||
message -> {
|
||||
askParam = message;
|
||||
caller = getSender();
|
||||
targetActor = getContext().actorOf(askParam.props);
|
||||
getContext().watch(targetActor);
|
||||
targetActor.forward(askParam.message, getContext());
|
||||
Scheduler scheduler = getContext().getSystem().scheduler();
|
||||
timeoutMessage =
|
||||
scheduler.scheduleOnce(
|
||||
askParam.timeout,
|
||||
getSelf(),
|
||||
new AskTimeout(),
|
||||
getContext().getDispatcher(),
|
||||
null);
|
||||
})
|
||||
.match(
|
||||
Terminated.class,
|
||||
message -> {
|
||||
Throwable ex = new ActorKilledException("Target actor terminated.");
|
||||
caller.tell(new Status.Failure(ex), getSelf());
|
||||
timeoutMessage.cancel();
|
||||
getContext().stop(getSelf());
|
||||
})
|
||||
.match(
|
||||
AskTimeout.class,
|
||||
message -> {
|
||||
Throwable ex =
|
||||
new TimeoutException(
|
||||
"Target actor timed out after " + askParam.timeout.toString());
|
||||
caller.tell(new Status.Failure(ex), getSelf());
|
||||
getContext().stop(getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
public static CompletionStage<Object> askOf(
|
||||
ActorRef supervisorCreator, Props props, Object message, Duration timeout) {
|
||||
AskParam param = new AskParam(props, message, timeout);
|
||||
return Patterns.ask(supervisorCreator, param, timeout);
|
||||
}
|
||||
|
||||
public static synchronized ActorRef createSupervisorCreator(ActorRefFactory factory) {
|
||||
return factory.actorOf(Props.create(AskSupervisorCreator.class));
|
||||
}
|
||||
}
|
||||
35
docs/src/test/java/jdocs/pattern/SupervisedAskSpec.java
Normal file
35
docs/src/test/java/jdocs/pattern/SupervisedAskSpec.java
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.pattern;
|
||||
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorRefFactory;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.actor.AbstractActor;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class SupervisedAskSpec {
|
||||
|
||||
public Object execute(
|
||||
Class<? extends AbstractActor> someActor,
|
||||
Object message,
|
||||
Duration timeout,
|
||||
ActorRefFactory actorSystem)
|
||||
throws Exception {
|
||||
// example usage
|
||||
try {
|
||||
ActorRef supervisorCreator = SupervisedAsk.createSupervisorCreator(actorSystem);
|
||||
CompletionStage<Object> finished =
|
||||
SupervisedAsk.askOf(supervisorCreator, Props.create(someActor), message, timeout);
|
||||
return finished.toCompletableFuture().get(timeout.toMillis(), TimeUnit.MILLISECONDS);
|
||||
} catch (Exception e) {
|
||||
// exception propagated by supervision
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,740 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.persistence;
|
||||
|
||||
import org.apache.pekko.actor.*;
|
||||
import org.apache.pekko.japi.Procedure;
|
||||
import org.apache.pekko.pattern.BackoffOpts;
|
||||
import org.apache.pekko.pattern.BackoffSupervisor;
|
||||
import org.apache.pekko.persistence.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
|
||||
public class LambdaPersistenceDocTest {
|
||||
|
||||
public interface SomeOtherMessage {}
|
||||
|
||||
public interface PersistentActorMethods {
|
||||
// #persistence-id
|
||||
public String persistenceId();
|
||||
|
||||
// #persistence-id
|
||||
}
|
||||
|
||||
public interface PersistenceActorRecoveryMethods {
|
||||
// #recovery-status
|
||||
public boolean recoveryRunning();
|
||||
|
||||
public boolean recoveryFinished();
|
||||
// #recovery-status
|
||||
}
|
||||
|
||||
static Object o2 =
|
||||
new Object() {
|
||||
abstract class MyPersistentActor1 extends AbstractPersistentActor {
|
||||
// #recovery-disabled
|
||||
@Override
|
||||
public Recovery recovery() {
|
||||
return Recovery.none();
|
||||
}
|
||||
// #recovery-disabled
|
||||
|
||||
// #recover-on-restart-disabled
|
||||
@Override
|
||||
public void preRestart(Throwable reason, Optional<Object> message) {}
|
||||
// #recover-on-restart-disabled
|
||||
}
|
||||
|
||||
abstract class MyPersistentActor2 extends AbstractPersistentActor {
|
||||
// #recovery-custom
|
||||
@Override
|
||||
public Recovery recovery() {
|
||||
return Recovery.create(457L);
|
||||
}
|
||||
// #recovery-custom
|
||||
}
|
||||
|
||||
class MyPersistentActor4 extends AbstractPersistentActor implements PersistentActorMethods {
|
||||
// #persistence-id-override
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "my-stable-persistence-id";
|
||||
}
|
||||
|
||||
// #persistence-id-override
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
cmd -> {
|
||||
/* ... */
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceiveRecover() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
evt -> {
|
||||
/* ... */
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// #recovery-completed
|
||||
class MyPersistentActor5 extends AbstractPersistentActor {
|
||||
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "my-stable-persistence-id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceiveRecover() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
RecoveryCompleted.class,
|
||||
r -> {
|
||||
// perform init after recovery, before any other messages
|
||||
// ...
|
||||
})
|
||||
.match(String.class, this::handleEvent)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(String.class, s -> s.equals("cmd"), s -> persist("evt", this::handleEvent))
|
||||
.build();
|
||||
}
|
||||
|
||||
private void handleEvent(String event) {
|
||||
// update state
|
||||
// ...
|
||||
}
|
||||
}
|
||||
// #recovery-completed
|
||||
|
||||
abstract class MyPersistentActor6 extends AbstractPersistentActor {
|
||||
// #recovery-no-snap
|
||||
@Override
|
||||
public Recovery recovery() {
|
||||
return Recovery.create(SnapshotSelectionCriteria.none());
|
||||
}
|
||||
// #recovery-no-snap
|
||||
}
|
||||
|
||||
abstract class MyActor extends AbstractPersistentActor {
|
||||
// #backoff
|
||||
@Override
|
||||
public void preStart() throws Exception {
|
||||
final Props childProps = Props.create(MyPersistentActor1.class);
|
||||
final Props props =
|
||||
BackoffSupervisor.props(
|
||||
BackoffOpts.onStop(
|
||||
childProps, "myActor", Duration.ofSeconds(3), Duration.ofSeconds(30), 0.2));
|
||||
getContext().actorOf(props, "mySupervisor");
|
||||
super.preStart();
|
||||
}
|
||||
// #backoff
|
||||
}
|
||||
};
|
||||
|
||||
static Object atLeastOnceExample =
|
||||
new Object() {
|
||||
// #at-least-once-example
|
||||
|
||||
class Msg implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
public final long deliveryId;
|
||||
public final String s;
|
||||
|
||||
public Msg(long deliveryId, String s) {
|
||||
this.deliveryId = deliveryId;
|
||||
this.s = s;
|
||||
}
|
||||
}
|
||||
|
||||
class Confirm implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
public final long deliveryId;
|
||||
|
||||
public Confirm(long deliveryId) {
|
||||
this.deliveryId = deliveryId;
|
||||
}
|
||||
}
|
||||
|
||||
class MsgSent implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
public final String s;
|
||||
|
||||
public MsgSent(String s) {
|
||||
this.s = s;
|
||||
}
|
||||
}
|
||||
|
||||
class MsgConfirmed implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
public final long deliveryId;
|
||||
|
||||
public MsgConfirmed(long deliveryId) {
|
||||
this.deliveryId = deliveryId;
|
||||
}
|
||||
}
|
||||
|
||||
class MyPersistentActor extends AbstractPersistentActorWithAtLeastOnceDelivery {
|
||||
private final ActorSelection destination;
|
||||
|
||||
public MyPersistentActor(ActorSelection destination) {
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "persistence-id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
s -> {
|
||||
persist(new MsgSent(s), evt -> updateState(evt));
|
||||
})
|
||||
.match(
|
||||
Confirm.class,
|
||||
confirm -> {
|
||||
persist(new MsgConfirmed(confirm.deliveryId), evt -> updateState(evt));
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceiveRecover() {
|
||||
return receiveBuilder().match(Object.class, evt -> updateState(evt)).build();
|
||||
}
|
||||
|
||||
void updateState(Object event) {
|
||||
if (event instanceof MsgSent) {
|
||||
final MsgSent evt = (MsgSent) event;
|
||||
deliver(destination, deliveryId -> new Msg(deliveryId, evt.s));
|
||||
} else if (event instanceof MsgConfirmed) {
|
||||
final MsgConfirmed evt = (MsgConfirmed) event;
|
||||
confirmDelivery(evt.deliveryId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MyDestination extends AbstractActor {
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Msg.class,
|
||||
msg -> {
|
||||
// ...
|
||||
getSender().tell(new Confirm(msg.deliveryId), getSelf());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #at-least-once-example
|
||||
|
||||
};
|
||||
|
||||
static Object o4 =
|
||||
new Object() {
|
||||
class MyPersistentActor extends AbstractPersistentActor {
|
||||
|
||||
private void updateState(String evt) {}
|
||||
|
||||
// #save-snapshot
|
||||
private Object state;
|
||||
private int snapShotInterval = 1000;
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
SaveSnapshotSuccess.class,
|
||||
ss -> {
|
||||
SnapshotMetadata metadata = ss.metadata();
|
||||
// ...
|
||||
})
|
||||
.match(
|
||||
SaveSnapshotFailure.class,
|
||||
sf -> {
|
||||
SnapshotMetadata metadata = sf.metadata();
|
||||
// ...
|
||||
})
|
||||
.match(
|
||||
String.class,
|
||||
cmd -> {
|
||||
persist(
|
||||
"evt-" + cmd,
|
||||
e -> {
|
||||
updateState(e);
|
||||
if (lastSequenceNr() % snapShotInterval == 0 && lastSequenceNr() != 0)
|
||||
saveSnapshot(state);
|
||||
});
|
||||
})
|
||||
.build();
|
||||
}
|
||||
// #save-snapshot
|
||||
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "persistence-id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceiveRecover() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
RecoveryCompleted.class,
|
||||
r -> {
|
||||
/* ...*/
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static Object o5 =
|
||||
new Object() {
|
||||
|
||||
class MyPersistentActor extends AbstractPersistentActor {
|
||||
|
||||
// #snapshot-criteria
|
||||
@Override
|
||||
public Recovery recovery() {
|
||||
return Recovery.create(
|
||||
SnapshotSelectionCriteria.create(457L, System.currentTimeMillis()));
|
||||
}
|
||||
// #snapshot-criteria
|
||||
|
||||
// #snapshot-offer
|
||||
private Object state;
|
||||
|
||||
@Override
|
||||
public Receive createReceiveRecover() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
SnapshotOffer.class,
|
||||
s -> {
|
||||
state = s.snapshot();
|
||||
// ...
|
||||
})
|
||||
.match(
|
||||
String.class,
|
||||
s -> {
|
||||
/* ...*/
|
||||
})
|
||||
.build();
|
||||
}
|
||||
// #snapshot-offer
|
||||
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "persistence-id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
s -> {
|
||||
/* ...*/
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
class MyActor extends AbstractActor {
|
||||
private final ActorRef persistentActor =
|
||||
getContext().actorOf(Props.create(MyPersistentActor.class));
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Object.class,
|
||||
o -> {
|
||||
/* ... */
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static Object o9 =
|
||||
new Object() {
|
||||
// #persist-async
|
||||
class MyPersistentActor extends AbstractPersistentActor {
|
||||
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "my-stable-persistence-id";
|
||||
}
|
||||
|
||||
private void handleCommand(String c) {
|
||||
getSender().tell(c, getSelf());
|
||||
|
||||
persistAsync(
|
||||
String.format("evt-%s-1", c),
|
||||
e -> {
|
||||
getSender().tell(e, getSelf());
|
||||
});
|
||||
persistAsync(
|
||||
String.format("evt-%s-2", c),
|
||||
e -> {
|
||||
getSender().tell(e, getSelf());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceiveRecover() {
|
||||
return receiveBuilder().match(String.class, this::handleCommand).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder().match(String.class, this::handleCommand).build();
|
||||
}
|
||||
}
|
||||
// #persist-async
|
||||
|
||||
public void usage() {
|
||||
final ActorSystem system = ActorSystem.create("example");
|
||||
// #persist-async-usage
|
||||
final ActorRef persistentActor = system.actorOf(Props.create(MyPersistentActor.class));
|
||||
persistentActor.tell("a", null);
|
||||
persistentActor.tell("b", null);
|
||||
|
||||
// possible order of received messages:
|
||||
// a
|
||||
// b
|
||||
// evt-a-1
|
||||
// evt-a-2
|
||||
// evt-b-1
|
||||
// evt-b-2
|
||||
// #persist-async-usage
|
||||
}
|
||||
};
|
||||
|
||||
static Object o10 =
|
||||
new Object() {
|
||||
// #defer
|
||||
class MyPersistentActor extends AbstractPersistentActor {
|
||||
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "my-stable-persistence-id";
|
||||
}
|
||||
|
||||
private void handleCommand(String c) {
|
||||
persistAsync(
|
||||
String.format("evt-%s-1", c),
|
||||
e -> {
|
||||
getSender().tell(e, getSelf());
|
||||
});
|
||||
persistAsync(
|
||||
String.format("evt-%s-2", c),
|
||||
e -> {
|
||||
getSender().tell(e, getSelf());
|
||||
});
|
||||
|
||||
deferAsync(
|
||||
String.format("evt-%s-3", c),
|
||||
e -> {
|
||||
getSender().tell(e, getSelf());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceiveRecover() {
|
||||
return receiveBuilder().match(String.class, this::handleCommand).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder().match(String.class, this::handleCommand).build();
|
||||
}
|
||||
}
|
||||
// #defer
|
||||
|
||||
public void usage() {
|
||||
final ActorSystem system = ActorSystem.create("example");
|
||||
final ActorRef sender = null; // your imaginary sender here
|
||||
// #defer-caller
|
||||
final ActorRef persistentActor = system.actorOf(Props.create(MyPersistentActor.class));
|
||||
persistentActor.tell("a", sender);
|
||||
persistentActor.tell("b", sender);
|
||||
|
||||
// order of received messages:
|
||||
// a
|
||||
// b
|
||||
// evt-a-1
|
||||
// evt-a-2
|
||||
// evt-a-3
|
||||
// evt-b-1
|
||||
// evt-b-2
|
||||
// evt-b-3
|
||||
// #defer-caller
|
||||
}
|
||||
};
|
||||
|
||||
static Object o100 =
|
||||
new Object() {
|
||||
// #defer-with-persist
|
||||
class MyPersistentActor extends AbstractPersistentActor {
|
||||
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "my-stable-persistence-id";
|
||||
}
|
||||
|
||||
private void handleCommand(String c) {
|
||||
persist(
|
||||
String.format("evt-%s-1", c),
|
||||
e -> {
|
||||
sender().tell(e, self());
|
||||
});
|
||||
persist(
|
||||
String.format("evt-%s-2", c),
|
||||
e -> {
|
||||
sender().tell(e, self());
|
||||
});
|
||||
|
||||
defer(
|
||||
String.format("evt-%s-3", c),
|
||||
e -> {
|
||||
sender().tell(e, self());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceiveRecover() {
|
||||
return receiveBuilder().match(String.class, this::handleCommand).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder().match(String.class, this::handleCommand).build();
|
||||
}
|
||||
}
|
||||
// #defer-with-persist
|
||||
|
||||
};
|
||||
|
||||
static Object o11 =
|
||||
new Object() {
|
||||
|
||||
class MyPersistentActor extends AbstractPersistentActor {
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "my-stable-persistence-id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder().matchAny(event -> {}).build();
|
||||
}
|
||||
|
||||
// #nested-persist-persist
|
||||
@Override
|
||||
public Receive createReceiveRecover() {
|
||||
final Procedure<String> replyToSender = event -> getSender().tell(event, getSelf());
|
||||
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
msg -> {
|
||||
persist(
|
||||
String.format("%s-outer-1", msg),
|
||||
event -> {
|
||||
getSender().tell(event, getSelf());
|
||||
persist(String.format("%s-inner-1", event), replyToSender);
|
||||
});
|
||||
|
||||
persist(
|
||||
String.format("%s-outer-2", msg),
|
||||
event -> {
|
||||
getSender().tell(event, getSelf());
|
||||
persist(String.format("%s-inner-2", event), replyToSender);
|
||||
});
|
||||
})
|
||||
.build();
|
||||
}
|
||||
// #nested-persist-persist
|
||||
|
||||
void usage(ActorRef persistentActor) {
|
||||
// #nested-persist-persist-caller
|
||||
persistentActor.tell("a", ActorRef.noSender());
|
||||
persistentActor.tell("b", ActorRef.noSender());
|
||||
|
||||
// order of received messages:
|
||||
// a
|
||||
// a-outer-1
|
||||
// a-outer-2
|
||||
// a-inner-1
|
||||
// a-inner-2
|
||||
// and only then process "b"
|
||||
// b
|
||||
// b-outer-1
|
||||
// b-outer-2
|
||||
// b-inner-1
|
||||
// b-inner-2
|
||||
|
||||
// #nested-persist-persist-caller
|
||||
}
|
||||
}
|
||||
|
||||
class MyPersistAsyncActor extends AbstractPersistentActor {
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "my-stable-persistence-id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceiveRecover() {
|
||||
return receiveBuilder().matchAny(event -> {}).build();
|
||||
}
|
||||
|
||||
// #nested-persistAsync-persistAsync
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
final Procedure<String> replyToSender = event -> getSender().tell(event, getSelf());
|
||||
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
String.class,
|
||||
msg -> {
|
||||
persistAsync(
|
||||
String.format("%s-outer-1", msg),
|
||||
event -> {
|
||||
getSender().tell(event, getSelf());
|
||||
persistAsync(String.format("%s-inner-1", event), replyToSender);
|
||||
});
|
||||
|
||||
persistAsync(
|
||||
String.format("%s-outer-2", msg),
|
||||
event -> {
|
||||
getSender().tell(event, getSelf());
|
||||
persistAsync(String.format("%s-inner-1", event), replyToSender);
|
||||
});
|
||||
})
|
||||
.build();
|
||||
}
|
||||
// #nested-persistAsync-persistAsync
|
||||
|
||||
void usage(ActorRef persistentActor) {
|
||||
// #nested-persistAsync-persistAsync-caller
|
||||
persistentActor.tell("a", getSelf());
|
||||
persistentActor.tell("b", getSelf());
|
||||
|
||||
// order of received messages:
|
||||
// a
|
||||
// b
|
||||
// a-outer-1
|
||||
// a-outer-2
|
||||
// b-outer-1
|
||||
// b-outer-2
|
||||
// a-inner-1
|
||||
// a-inner-2
|
||||
// b-inner-1
|
||||
// b-inner-2
|
||||
|
||||
// which can be seen as the following causal relationship:
|
||||
// a -> a-outer-1 -> a-outer-2 -> a-inner-1 -> a-inner-2
|
||||
// b -> b-outer-1 -> b-outer-2 -> b-inner-1 -> b-inner-2
|
||||
|
||||
// #nested-persistAsync-persistAsync-caller
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static Object o14 =
|
||||
new Object() {
|
||||
// #safe-shutdown
|
||||
final class Shutdown {}
|
||||
|
||||
class MyPersistentActor extends AbstractPersistentActor {
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "some-persistence-id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Shutdown.class,
|
||||
shutdown -> {
|
||||
getContext().stop(getSelf());
|
||||
})
|
||||
.match(
|
||||
String.class,
|
||||
msg -> {
|
||||
System.out.println(msg);
|
||||
persist("handle-" + msg, e -> System.out.println(e));
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceiveRecover() {
|
||||
return receiveBuilder().matchAny(any -> {}).build();
|
||||
}
|
||||
}
|
||||
// #safe-shutdown
|
||||
|
||||
public void usage() {
|
||||
final ActorSystem system = ActorSystem.create("example");
|
||||
final ActorRef persistentActor = system.actorOf(Props.create(MyPersistentActor.class));
|
||||
// #safe-shutdown-example-bad
|
||||
// UN-SAFE, due to PersistentActor's command stashing:
|
||||
persistentActor.tell("a", ActorRef.noSender());
|
||||
persistentActor.tell("b", ActorRef.noSender());
|
||||
persistentActor.tell(PoisonPill.getInstance(), ActorRef.noSender());
|
||||
// order of received messages:
|
||||
// a
|
||||
// # b arrives at mailbox, stashing; internal-stash = [b]
|
||||
// # PoisonPill arrives at mailbox, stashing; internal-stash = [b, Shutdown]
|
||||
// PoisonPill is an AutoReceivedMessage, is handled automatically
|
||||
// !! stop !!
|
||||
// Actor is stopped without handling `b` nor the `a` handler!
|
||||
// #safe-shutdown-example-bad
|
||||
|
||||
// #safe-shutdown-example-good
|
||||
// SAFE:
|
||||
persistentActor.tell("a", ActorRef.noSender());
|
||||
persistentActor.tell("b", ActorRef.noSender());
|
||||
persistentActor.tell(new Shutdown(), ActorRef.noSender());
|
||||
// order of received messages:
|
||||
// a
|
||||
// # b arrives at mailbox, stashing; internal-stash = [b]
|
||||
// # Shutdown arrives at mailbox, stashing; internal-stash = [b, Shutdown]
|
||||
// handle-a
|
||||
// # unstashing; internal-stash = [Shutdown]
|
||||
// b
|
||||
// handle-b
|
||||
// # unstashing; internal-stash = []
|
||||
// Shutdown
|
||||
// -- stop --
|
||||
// #safe-shutdown-example-good
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.persistence;
|
||||
|
||||
// #plugin-imports
|
||||
import org.apache.pekko.dispatch.Futures;
|
||||
import org.apache.pekko.persistence.*;
|
||||
import org.apache.pekko.persistence.journal.japi.*;
|
||||
import org.apache.pekko.persistence.snapshot.japi.*;
|
||||
// #plugin-imports
|
||||
|
||||
import org.apache.pekko.actor.*;
|
||||
import org.apache.pekko.persistence.journal.leveldb.SharedLeveldbJournal;
|
||||
import org.apache.pekko.persistence.journal.leveldb.SharedLeveldbStore;
|
||||
|
||||
import com.typesafe.config.Config;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.runner.RunWith;
|
||||
import org.scalatestplus.junit.JUnitRunner;
|
||||
import scala.concurrent.Future;
|
||||
import java.util.function.Consumer;
|
||||
import org.iq80.leveldb.util.FileUtils;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.pekko.persistence.japi.journal.JavaJournalSpec;
|
||||
import org.apache.pekko.persistence.japi.snapshot.JavaSnapshotStoreSpec;
|
||||
|
||||
public class LambdaPersistencePluginDocTest {
|
||||
|
||||
static Object o1 =
|
||||
new Object() {
|
||||
final ActorSystem system = null;
|
||||
// #shared-store-creation
|
||||
final ActorRef store = system.actorOf(Props.create(SharedLeveldbStore.class), "store");
|
||||
// #shared-store-creation
|
||||
|
||||
// #shared-store-usage
|
||||
class SharedStorageUsage extends AbstractActor {
|
||||
@Override
|
||||
public void preStart() throws Exception {
|
||||
String path = "akka://example@127.0.0.1:2552/user/store";
|
||||
ActorSelection selection = getContext().actorSelection(path);
|
||||
selection.tell(new Identify(1), getSelf());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
ActorIdentity.class,
|
||||
ai -> {
|
||||
if (ai.correlationId().equals(1)) {
|
||||
Optional<ActorRef> store = ai.getActorRef();
|
||||
if (store.isPresent()) {
|
||||
SharedLeveldbJournal.setStore(store.get(), getContext().getSystem());
|
||||
} else {
|
||||
throw new RuntimeException("Couldn't identify store");
|
||||
}
|
||||
}
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #shared-store-usage
|
||||
};
|
||||
|
||||
class MySnapshotStore extends SnapshotStore {
|
||||
@Override
|
||||
public Future<Optional<SelectedSnapshot>> doLoadAsync(
|
||||
String persistenceId, SnapshotSelectionCriteria criteria) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Void> doSaveAsync(SnapshotMetadata metadata, Object snapshot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Void> doDeleteAsync(SnapshotMetadata metadata) {
|
||||
return Futures.successful(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Void> doDeleteAsync(String persistenceId, SnapshotSelectionCriteria criteria) {
|
||||
return Futures.successful(null);
|
||||
}
|
||||
}
|
||||
|
||||
class MyAsyncJournal extends AsyncWriteJournal {
|
||||
// #sync-journal-plugin-api
|
||||
@Override
|
||||
public Future<Iterable<Optional<Exception>>> doAsyncWriteMessages(
|
||||
Iterable<AtomicWrite> messages) {
|
||||
try {
|
||||
Iterable<Optional<Exception>> result = new ArrayList<Optional<Exception>>();
|
||||
// blocking call here...
|
||||
// result.add(..)
|
||||
return Futures.successful(result);
|
||||
} catch (Exception e) {
|
||||
return Futures.failed(e);
|
||||
}
|
||||
}
|
||||
// #sync-journal-plugin-api
|
||||
|
||||
@Override
|
||||
public Future<Void> doAsyncDeleteMessagesTo(String persistenceId, long toSequenceNr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Void> doAsyncReplayMessages(
|
||||
String persistenceId,
|
||||
long fromSequenceNr,
|
||||
long toSequenceNr,
|
||||
long max,
|
||||
Consumer<PersistentRepr> replayCallback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Long> doAsyncReadHighestSequenceNr(String persistenceId, long fromSequenceNr) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Object o2 =
|
||||
new Object() {
|
||||
// #journal-tck-java
|
||||
@RunWith(JUnitRunner.class)
|
||||
class MyJournalSpecTest extends JavaJournalSpec {
|
||||
|
||||
public MyJournalSpecTest() {
|
||||
super(
|
||||
ConfigFactory.parseString(
|
||||
"akka.persistence.journal.plugin = "
|
||||
+ "\"akka.persistence.journal.leveldb-shared\""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CapabilityFlag supportsRejectingNonSerializableObjects() {
|
||||
return CapabilityFlag.off();
|
||||
}
|
||||
}
|
||||
// #journal-tck-java
|
||||
};
|
||||
|
||||
static Object o3 =
|
||||
new Object() {
|
||||
// #snapshot-store-tck-java
|
||||
@RunWith(JUnitRunner.class)
|
||||
class MySnapshotStoreTest extends JavaSnapshotStoreSpec {
|
||||
|
||||
public MySnapshotStoreTest() {
|
||||
super(
|
||||
ConfigFactory.parseString(
|
||||
"akka.persistence.snapshot-store.plugin = "
|
||||
+ "\"akka.persistence.snapshot-store.local\""));
|
||||
}
|
||||
}
|
||||
// #snapshot-store-tck-java
|
||||
};
|
||||
|
||||
static Object o4 =
|
||||
new Object() {
|
||||
// https://github.com/akka/akka/issues/26826
|
||||
// #journal-tck-before-after-java
|
||||
@RunWith(JUnitRunner.class)
|
||||
class MyJournalSpecTest extends JavaJournalSpec {
|
||||
|
||||
List<File> storageLocations = new ArrayList<File>();
|
||||
|
||||
public MyJournalSpecTest() {
|
||||
super(
|
||||
ConfigFactory.parseString(
|
||||
"persistence.journal.plugin = "
|
||||
+ "\"akka.persistence.journal.leveldb-shared\""));
|
||||
|
||||
Config config = system().settings().config();
|
||||
storageLocations.add(
|
||||
new File(config.getString("akka.persistence.journal.leveldb.dir")));
|
||||
storageLocations.add(
|
||||
new File(config.getString("akka.persistence.snapshot-store.local.dir")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CapabilityFlag supportsRejectingNonSerializableObjects() {
|
||||
return CapabilityFlag.on();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeAll() {
|
||||
for (File storageLocation : storageLocations) {
|
||||
FileUtils.deleteRecursively(storageLocation);
|
||||
}
|
||||
super.beforeAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterAll() {
|
||||
super.afterAll();
|
||||
for (File storageLocation : storageLocations) {
|
||||
FileUtils.deleteRecursively(storageLocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
// #journal-tck-before-after-java
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (C) 2015-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.persistence;
|
||||
|
||||
import org.apache.pekko.persistence.journal.EventAdapter;
|
||||
import org.apache.pekko.persistence.journal.EventSeq;
|
||||
|
||||
public class PersistenceEventAdapterDocTest {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
static
|
||||
// #identity-event-adapter
|
||||
class MyEventAdapter implements EventAdapter {
|
||||
@Override
|
||||
public String manifest(Object event) {
|
||||
return ""; // if no manifest needed, return ""
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object toJournal(Object event) {
|
||||
return event; // identity
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventSeq fromJournal(Object event, String manifest) {
|
||||
return EventSeq.single(event); // identity
|
||||
}
|
||||
}
|
||||
// #identity-event-adapter
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.persistence;
|
||||
|
||||
import org.apache.pekko.persistence.AbstractPersistentActor;
|
||||
import org.apache.pekko.persistence.RuntimePluginConfig;
|
||||
import com.typesafe.config.Config;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
|
||||
public class PersistenceMultiDocTest {
|
||||
|
||||
// #default-plugins
|
||||
abstract class AbstractPersistentActorWithDefaultPlugins extends AbstractPersistentActor {
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "123";
|
||||
}
|
||||
}
|
||||
// #default-plugins
|
||||
|
||||
// #override-plugins
|
||||
abstract class AbstractPersistentActorWithOverridePlugins extends AbstractPersistentActor {
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "123";
|
||||
}
|
||||
|
||||
// Absolute path to the journal plugin configuration entry in the `reference.conf`
|
||||
@Override
|
||||
public String journalPluginId() {
|
||||
return "akka.persistence.chronicle.journal";
|
||||
}
|
||||
|
||||
// Absolute path to the snapshot store plugin configuration entry in the `reference.conf`
|
||||
@Override
|
||||
public String snapshotPluginId() {
|
||||
return "akka.persistence.chronicle.snapshot-store";
|
||||
}
|
||||
}
|
||||
// #override-plugins
|
||||
|
||||
// #runtime-config
|
||||
abstract class AbstractPersistentActorWithRuntimePluginConfig extends AbstractPersistentActor
|
||||
implements RuntimePluginConfig {
|
||||
// Variable that is retrieved at runtime, from an external service for instance.
|
||||
String runtimeDistinction = "foo";
|
||||
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "123";
|
||||
}
|
||||
|
||||
// Absolute path to the journal plugin configuration entry in the `reference.conf`
|
||||
@Override
|
||||
public String journalPluginId() {
|
||||
return "journal-plugin-" + runtimeDistinction;
|
||||
}
|
||||
|
||||
// Absolute path to the snapshot store plugin configuration entry in the `reference.conf`
|
||||
@Override
|
||||
public String snapshotPluginId() {
|
||||
return "snapshot-store-plugin-" + runtimeDistinction;
|
||||
}
|
||||
|
||||
// Configuration which contains the journal plugin id defined above
|
||||
@Override
|
||||
public Config journalPluginConfig() {
|
||||
return ConfigFactory.empty()
|
||||
.withValue(
|
||||
"journal-plugin-" + runtimeDistinction,
|
||||
getContext()
|
||||
.getSystem()
|
||||
.settings()
|
||||
.config()
|
||||
.getValue(
|
||||
"journal-plugin") // or a very different configuration coming from an external
|
||||
// service.
|
||||
);
|
||||
}
|
||||
|
||||
// Configuration which contains the snapshot store plugin id defined above
|
||||
@Override
|
||||
public Config snapshotPluginConfig() {
|
||||
return ConfigFactory.empty()
|
||||
.withValue(
|
||||
"snapshot-plugin-" + runtimeDistinction,
|
||||
getContext()
|
||||
.getSystem()
|
||||
.settings()
|
||||
.config()
|
||||
.getValue(
|
||||
"snapshot-store-plugin") // or a very different configuration coming from an
|
||||
// external service.
|
||||
);
|
||||
}
|
||||
}
|
||||
// #runtime-config
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,422 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.persistence;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.time.Duration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.pekko.NotUsed;
|
||||
import org.apache.pekko.persistence.query.Sequence;
|
||||
import org.apache.pekko.persistence.query.Offset;
|
||||
import com.typesafe.config.Config;
|
||||
|
||||
import org.apache.pekko.actor.*;
|
||||
import org.apache.pekko.persistence.query.*;
|
||||
import org.apache.pekko.stream.javadsl.Sink;
|
||||
import org.apache.pekko.stream.javadsl.Source;
|
||||
|
||||
import jdocs.persistence.query.MyEventsByTagSource;
|
||||
import org.reactivestreams.Subscriber;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
public class PersistenceQueryDocTest {
|
||||
|
||||
final ActorSystem system = ActorSystem.create();
|
||||
|
||||
public
|
||||
// #advanced-journal-query-types
|
||||
static class RichEvent {
|
||||
public final Set<String> tags;
|
||||
public final Object payload;
|
||||
|
||||
public RichEvent(Set<String> tags, Object payload) {
|
||||
this.tags = tags;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
// #advanced-journal-query-types
|
||||
|
||||
public
|
||||
// #advanced-journal-query-types
|
||||
// a plugin can provide:
|
||||
static final class QueryMetadata {
|
||||
public final boolean deterministicOrder;
|
||||
public final boolean infinite;
|
||||
|
||||
public QueryMetadata(boolean deterministicOrder, boolean infinite) {
|
||||
this.deterministicOrder = deterministicOrder;
|
||||
this.infinite = infinite;
|
||||
}
|
||||
}
|
||||
// #advanced-journal-query-types
|
||||
|
||||
interface OrderCompleted {}
|
||||
|
||||
public
|
||||
// #my-read-journal
|
||||
static class MyReadJournalProvider implements ReadJournalProvider {
|
||||
private final MyJavadslReadJournal javadslReadJournal;
|
||||
|
||||
public MyReadJournalProvider(ExtendedActorSystem system, Config config) {
|
||||
this.javadslReadJournal = new MyJavadslReadJournal(system, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MyScaladslReadJournal scaladslReadJournal() {
|
||||
return new MyScaladslReadJournal(javadslReadJournal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MyJavadslReadJournal javadslReadJournal() {
|
||||
return this.javadslReadJournal;
|
||||
}
|
||||
}
|
||||
// #my-read-journal
|
||||
|
||||
public
|
||||
// #my-read-journal
|
||||
static class MyJavadslReadJournal
|
||||
implements org.apache.pekko.persistence.query.javadsl.ReadJournal,
|
||||
org.apache.pekko.persistence.query.javadsl.EventsByTagQuery,
|
||||
org.apache.pekko.persistence.query.javadsl.EventsByPersistenceIdQuery,
|
||||
org.apache.pekko.persistence.query.javadsl.PersistenceIdsQuery,
|
||||
org.apache.pekko.persistence.query.javadsl.CurrentPersistenceIdsQuery {
|
||||
|
||||
private final Duration refreshInterval;
|
||||
private Connection conn;
|
||||
|
||||
public MyJavadslReadJournal(ExtendedActorSystem system, Config config) {
|
||||
refreshInterval = config.getDuration("refresh-interval");
|
||||
}
|
||||
|
||||
/**
|
||||
* You can use `NoOffset` to retrieve all events with a given tag or retrieve a subset of all
|
||||
* events by specifying a `Sequence` `offset`. The `offset` corresponds to an ordered sequence
|
||||
* number for the specific tag. Note that the corresponding offset of each event is provided in
|
||||
* the [[akka.persistence.query.EventEnvelope]], which makes it possible to resume the stream at
|
||||
* a later point from a given offset.
|
||||
*
|
||||
* <p>The `offset` is exclusive, i.e. the event with the exact same sequence number will not be
|
||||
* included in the returned stream. This means that you can use the offset that is returned in
|
||||
* `EventEnvelope` as the `offset` parameter in a subsequent query.
|
||||
*/
|
||||
@Override
|
||||
public Source<EventEnvelope, NotUsed> eventsByTag(String tag, Offset offset) {
|
||||
if (offset instanceof Sequence) {
|
||||
Sequence sequenceOffset = (Sequence) offset;
|
||||
return Source.fromGraph(
|
||||
new MyEventsByTagSource(conn, tag, sequenceOffset.value(), refreshInterval));
|
||||
} else if (offset == NoOffset.getInstance())
|
||||
return eventsByTag(tag, Offset.sequence(0L)); // recursive
|
||||
else
|
||||
throw new IllegalArgumentException(
|
||||
"MyJavadslReadJournal does not support " + offset.getClass().getName() + " offsets");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Source<EventEnvelope, NotUsed> eventsByPersistenceId(
|
||||
String persistenceId, long fromSequenceNr, long toSequenceNr) {
|
||||
// implement in a similar way as eventsByTag
|
||||
throw new UnsupportedOperationException("Not implemented yet");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Source<String, NotUsed> persistenceIds() {
|
||||
// implement in a similar way as eventsByTag
|
||||
throw new UnsupportedOperationException("Not implemented yet");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Source<String, NotUsed> currentPersistenceIds() {
|
||||
// implement in a similar way as eventsByTag
|
||||
throw new UnsupportedOperationException("Not implemented yet");
|
||||
}
|
||||
|
||||
// possibility to add more plugin specific queries
|
||||
|
||||
// #advanced-journal-query-definition
|
||||
public Source<RichEvent, QueryMetadata> byTagsWithMeta(Set<String> tags) {
|
||||
// #advanced-journal-query-definition
|
||||
// implement in a similar way as eventsByTag
|
||||
throw new UnsupportedOperationException("Not implemented yet");
|
||||
}
|
||||
}
|
||||
// #my-read-journal
|
||||
|
||||
public
|
||||
// #my-read-journal
|
||||
static class MyScaladslReadJournal
|
||||
implements org.apache.pekko.persistence.query.scaladsl.ReadJournal,
|
||||
org.apache.pekko.persistence.query.scaladsl.EventsByTagQuery,
|
||||
org.apache.pekko.persistence.query.scaladsl.EventsByPersistenceIdQuery,
|
||||
org.apache.pekko.persistence.query.scaladsl.PersistenceIdsQuery,
|
||||
org.apache.pekko.persistence.query.scaladsl.CurrentPersistenceIdsQuery {
|
||||
|
||||
private final MyJavadslReadJournal javadslReadJournal;
|
||||
|
||||
public MyScaladslReadJournal(MyJavadslReadJournal javadslReadJournal) {
|
||||
this.javadslReadJournal = javadslReadJournal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.apache.pekko.stream.scaladsl.Source<EventEnvelope, NotUsed> eventsByTag(
|
||||
String tag, org.apache.pekko.persistence.query.Offset offset) {
|
||||
return javadslReadJournal.eventsByTag(tag, offset).asScala();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.apache.pekko.stream.scaladsl.Source<EventEnvelope, NotUsed> eventsByPersistenceId(
|
||||
String persistenceId, long fromSequenceNr, long toSequenceNr) {
|
||||
return javadslReadJournal
|
||||
.eventsByPersistenceId(persistenceId, fromSequenceNr, toSequenceNr)
|
||||
.asScala();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.apache.pekko.stream.scaladsl.Source<String, NotUsed> persistenceIds() {
|
||||
return javadslReadJournal.persistenceIds().asScala();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.apache.pekko.stream.scaladsl.Source<String, NotUsed> currentPersistenceIds() {
|
||||
return javadslReadJournal.currentPersistenceIds().asScala();
|
||||
}
|
||||
|
||||
// possibility to add more plugin specific queries
|
||||
|
||||
public org.apache.pekko.stream.scaladsl.Source<RichEvent, QueryMetadata> byTagsWithMeta(
|
||||
scala.collection.Set<String> tags) {
|
||||
Set<String> jTags = scala.collection.JavaConverters.setAsJavaSetConverter(tags).asJava();
|
||||
return javadslReadJournal.byTagsWithMeta(jTags).asScala();
|
||||
}
|
||||
}
|
||||
// #my-read-journal
|
||||
|
||||
void demonstrateBasicUsage() {
|
||||
final ActorSystem system = ActorSystem.create();
|
||||
|
||||
// #basic-usage
|
||||
// obtain read journal by plugin id
|
||||
final MyJavadslReadJournal readJournal =
|
||||
PersistenceQuery.get(system)
|
||||
.getReadJournalFor(
|
||||
MyJavadslReadJournal.class, "akka.persistence.query.my-read-journal");
|
||||
|
||||
// issue query to journal
|
||||
Source<EventEnvelope, NotUsed> source =
|
||||
readJournal.eventsByPersistenceId("user-1337", 0, Long.MAX_VALUE);
|
||||
|
||||
// materialize stream, consuming events
|
||||
source.runForeach(event -> System.out.println("Event: " + event), system);
|
||||
// #basic-usage
|
||||
}
|
||||
|
||||
void demonstrateAllPersistenceIdsLive() {
|
||||
final MyJavadslReadJournal readJournal =
|
||||
PersistenceQuery.get(system)
|
||||
.getReadJournalFor(
|
||||
MyJavadslReadJournal.class, "akka.persistence.query.my-read-journal");
|
||||
|
||||
// #all-persistence-ids-live
|
||||
readJournal.persistenceIds();
|
||||
// #all-persistence-ids-live
|
||||
}
|
||||
|
||||
void demonstrateNoRefresh() {
|
||||
final ActorSystem system = ActorSystem.create();
|
||||
|
||||
final MyJavadslReadJournal readJournal =
|
||||
PersistenceQuery.get(system)
|
||||
.getReadJournalFor(
|
||||
MyJavadslReadJournal.class, "akka.persistence.query.my-read-journal");
|
||||
|
||||
// #all-persistence-ids-snap
|
||||
readJournal.currentPersistenceIds();
|
||||
// #all-persistence-ids-snap
|
||||
}
|
||||
|
||||
void demonstrateRefresh() {
|
||||
final ActorSystem system = ActorSystem.create();
|
||||
|
||||
final MyJavadslReadJournal readJournal =
|
||||
PersistenceQuery.get(system)
|
||||
.getReadJournalFor(
|
||||
MyJavadslReadJournal.class, "akka.persistence.query.my-read-journal");
|
||||
|
||||
// #events-by-persistent-id
|
||||
readJournal.eventsByPersistenceId("user-us-1337", 0L, Long.MAX_VALUE);
|
||||
// #events-by-persistent-id
|
||||
}
|
||||
|
||||
void demonstrateEventsByTag() {
|
||||
final ActorSystem system = ActorSystem.create();
|
||||
|
||||
final MyJavadslReadJournal readJournal =
|
||||
PersistenceQuery.get(system)
|
||||
.getReadJournalFor(
|
||||
MyJavadslReadJournal.class, "akka.persistence.query.my-read-journal");
|
||||
|
||||
// #events-by-tag
|
||||
// assuming journal is able to work with numeric offsets we can:
|
||||
final Source<EventEnvelope, NotUsed> completedOrders =
|
||||
readJournal.eventsByTag("order-completed", new Sequence(0L));
|
||||
|
||||
// find first 10 completed orders:
|
||||
final CompletionStage<List<OrderCompleted>> firstCompleted =
|
||||
completedOrders
|
||||
.map(EventEnvelope::event)
|
||||
.collectType(OrderCompleted.class)
|
||||
.take(10) // cancels the query stream after pulling 10 elements
|
||||
.runFold(
|
||||
new ArrayList<>(10),
|
||||
(acc, e) -> {
|
||||
acc.add(e);
|
||||
return acc;
|
||||
},
|
||||
system);
|
||||
|
||||
// start another query, from the known offset
|
||||
Source<EventEnvelope, NotUsed> furtherOrders =
|
||||
readJournal.eventsByTag("order-completed", new Sequence(10));
|
||||
// #events-by-tag
|
||||
}
|
||||
|
||||
void demonstrateMaterializedQueryValues() {
|
||||
final ActorSystem system = ActorSystem.create();
|
||||
|
||||
final MyJavadslReadJournal readJournal =
|
||||
PersistenceQuery.get(system)
|
||||
.getReadJournalFor(
|
||||
MyJavadslReadJournal.class, "akka.persistence.query.my-read-journal");
|
||||
|
||||
// #advanced-journal-query-usage
|
||||
|
||||
Set<String> tags = new HashSet<String>();
|
||||
tags.add("red");
|
||||
tags.add("blue");
|
||||
final Source<RichEvent, QueryMetadata> events =
|
||||
readJournal
|
||||
.byTagsWithMeta(tags)
|
||||
.mapMaterializedValue(
|
||||
meta -> {
|
||||
System.out.println(
|
||||
"The query is: "
|
||||
+ "ordered deterministically: "
|
||||
+ meta.deterministicOrder
|
||||
+ " "
|
||||
+ "infinite: "
|
||||
+ meta.infinite);
|
||||
return meta;
|
||||
});
|
||||
|
||||
events
|
||||
.map(
|
||||
event -> {
|
||||
System.out.println("Event payload: " + event.payload);
|
||||
return event.payload;
|
||||
})
|
||||
.runWith(Sink.ignore(), system);
|
||||
|
||||
// #advanced-journal-query-usage
|
||||
}
|
||||
|
||||
class ReactiveStreamsCompatibleDBDriver {
|
||||
Subscriber<List<Object>> batchWriter() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void demonstrateWritingIntoDifferentStore() {
|
||||
final ActorSystem system = ActorSystem.create();
|
||||
|
||||
final MyJavadslReadJournal readJournal =
|
||||
PersistenceQuery.get(system)
|
||||
.getReadJournalFor(
|
||||
MyJavadslReadJournal.class, "akka.persistence.query.my-read-journal");
|
||||
|
||||
// #projection-into-different-store-rs
|
||||
final ReactiveStreamsCompatibleDBDriver driver = new ReactiveStreamsCompatibleDBDriver();
|
||||
final Subscriber<List<Object>> dbBatchWriter = driver.batchWriter();
|
||||
|
||||
// Using an example (Reactive Streams) Database driver
|
||||
readJournal
|
||||
.eventsByPersistenceId("user-1337", 0L, Long.MAX_VALUE)
|
||||
.map(envelope -> envelope.event())
|
||||
.grouped(20) // batch inserts into groups of 20
|
||||
.runWith(Sink.fromSubscriber(dbBatchWriter), system); // write batches to read-side database
|
||||
// #projection-into-different-store-rs
|
||||
}
|
||||
|
||||
// #projection-into-different-store-simple-classes
|
||||
static class ExampleStore {
|
||||
CompletionStage<Void> save(Object any) {
|
||||
// ...
|
||||
// #projection-into-different-store-simple-classes
|
||||
return null;
|
||||
// #projection-into-different-store-simple-classes
|
||||
}
|
||||
}
|
||||
// #projection-into-different-store-simple-classes
|
||||
|
||||
void demonstrateWritingIntoDifferentStoreWithMapAsync() {
|
||||
final ActorSystem system = ActorSystem.create();
|
||||
|
||||
final MyJavadslReadJournal readJournal =
|
||||
PersistenceQuery.get(system)
|
||||
.getReadJournalFor(
|
||||
MyJavadslReadJournal.class, "akka.persistence.query.my-read-journal");
|
||||
|
||||
// #projection-into-different-store-simple
|
||||
final ExampleStore store = new ExampleStore();
|
||||
|
||||
readJournal
|
||||
.eventsByTag("bid", new Sequence(0L))
|
||||
.mapAsync(1, store::save)
|
||||
.runWith(Sink.ignore(), system);
|
||||
// #projection-into-different-store-simple
|
||||
}
|
||||
|
||||
// #projection-into-different-store
|
||||
static class MyResumableProjection {
|
||||
private final String name;
|
||||
|
||||
public MyResumableProjection(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public CompletionStage<Long> saveProgress(Offset offset) {
|
||||
// ...
|
||||
// #projection-into-different-store
|
||||
return null;
|
||||
// #projection-into-different-store
|
||||
}
|
||||
|
||||
public CompletionStage<Long> latestOffset() {
|
||||
// ...
|
||||
// #projection-into-different-store
|
||||
return null;
|
||||
// #projection-into-different-store
|
||||
}
|
||||
}
|
||||
// #projection-into-different-store
|
||||
|
||||
static class ComplexState {
|
||||
|
||||
boolean readyToSave() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static class Record {
|
||||
static Record of(Object any) {
|
||||
return new Record();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,547 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.persistence;
|
||||
|
||||
import docs.persistence.ExampleJsonMarshaller;
|
||||
import docs.persistence.proto.FlightAppModels;
|
||||
|
||||
import java.io.NotSerializableException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import spray.json.JsObject;
|
||||
|
||||
import org.apache.pekko.persistence.journal.EventAdapter;
|
||||
import org.apache.pekko.persistence.journal.EventSeq;
|
||||
import org.apache.pekko.protobufv3.internal.InvalidProtocolBufferException;
|
||||
import org.apache.pekko.serialization.SerializerWithStringManifest;
|
||||
|
||||
public class PersistenceSchemaEvolutionDocTest {
|
||||
|
||||
public
|
||||
// #protobuf-read-optional-model
|
||||
static enum SeatType {
|
||||
Window("W"),
|
||||
Aisle("A"),
|
||||
Other("O"),
|
||||
Unknown("");
|
||||
|
||||
private final String code;
|
||||
|
||||
private SeatType(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public static SeatType fromCode(String c) {
|
||||
if (Window.code.equals(c)) return Window;
|
||||
else if (Aisle.code.equals(c)) return Aisle;
|
||||
else if (Other.code.equals(c)) return Other;
|
||||
else return Unknown;
|
||||
}
|
||||
}
|
||||
// #protobuf-read-optional-model
|
||||
|
||||
public
|
||||
// #protobuf-read-optional-model
|
||||
static class SeatReserved {
|
||||
public final String letter;
|
||||
public final int row;
|
||||
public final SeatType seatType;
|
||||
|
||||
public SeatReserved(String letter, int row, SeatType seatType) {
|
||||
this.letter = letter;
|
||||
this.row = row;
|
||||
this.seatType = seatType;
|
||||
}
|
||||
}
|
||||
// #protobuf-read-optional-model
|
||||
|
||||
public
|
||||
// #protobuf-read-optional
|
||||
/**
|
||||
* Example serializer impl which uses protocol buffers generated classes (proto.*) to perform the
|
||||
* to/from binary marshalling.
|
||||
*/
|
||||
static class AddedFieldsSerializerWithProtobuf extends SerializerWithStringManifest {
|
||||
@Override
|
||||
public int identifier() {
|
||||
return 67876;
|
||||
}
|
||||
|
||||
private final String seatReservedManifest = SeatReserved.class.getName();
|
||||
|
||||
@Override
|
||||
public String manifest(Object o) {
|
||||
return o.getClass().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromBinary(byte[] bytes, String manifest) throws NotSerializableException {
|
||||
if (seatReservedManifest.equals(manifest)) {
|
||||
// use generated protobuf serializer
|
||||
try {
|
||||
return seatReserved(FlightAppModels.SeatReserved.parseFrom(bytes));
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new IllegalArgumentException(e.getMessage());
|
||||
}
|
||||
} else {
|
||||
throw new NotSerializableException("Unable to handle manifest: " + manifest);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBinary(Object o) {
|
||||
if (o instanceof SeatReserved) {
|
||||
SeatReserved s = (SeatReserved) o;
|
||||
return FlightAppModels.SeatReserved.newBuilder()
|
||||
.setRow(s.row)
|
||||
.setLetter(s.letter)
|
||||
.setSeatType(s.seatType.code)
|
||||
.build()
|
||||
.toByteArray();
|
||||
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unable to handle: " + o);
|
||||
}
|
||||
}
|
||||
|
||||
// -- fromBinary helpers --
|
||||
|
||||
private SeatReserved seatReserved(FlightAppModels.SeatReserved p) {
|
||||
return new SeatReserved(p.getLetter(), p.getRow(), seatType(p));
|
||||
}
|
||||
|
||||
// handle missing field by assigning "Unknown" value
|
||||
private SeatType seatType(FlightAppModels.SeatReserved p) {
|
||||
if (p.hasSeatType()) return SeatType.fromCode(p.getSeatType());
|
||||
else return SeatType.Unknown;
|
||||
}
|
||||
}
|
||||
// #protobuf-read-optional
|
||||
|
||||
public static class RenamePlainJson {
|
||||
public
|
||||
// #rename-plain-json
|
||||
static class JsonRenamedFieldAdapter implements EventAdapter {
|
||||
// use your favorite json library
|
||||
private final ExampleJsonMarshaller marshaller = new ExampleJsonMarshaller();
|
||||
|
||||
private final String V1 = "v1";
|
||||
private final String V2 = "v2";
|
||||
|
||||
// this could be done independently for each event type
|
||||
@Override
|
||||
public String manifest(Object event) {
|
||||
return V2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsObject toJournal(Object event) {
|
||||
return marshaller.toJson(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventSeq fromJournal(Object event, String manifest) {
|
||||
if (event instanceof JsObject) {
|
||||
JsObject json = (JsObject) event;
|
||||
if (V1.equals(manifest)) json = rename(json, "code", "seatNr");
|
||||
return EventSeq.single(json);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Can only work with JSON, was: " + event.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private JsObject rename(JsObject json, String from, String to) {
|
||||
// use your favorite json library to rename the field
|
||||
JsObject renamed = json;
|
||||
return renamed;
|
||||
}
|
||||
}
|
||||
// #rename-plain-json
|
||||
}
|
||||
|
||||
public static class SimplestCustomSerializer {
|
||||
|
||||
public
|
||||
// #simplest-custom-serializer-model
|
||||
static class Person {
|
||||
public final String name;
|
||||
public final String surname;
|
||||
|
||||
public Person(String name, String surname) {
|
||||
this.name = name;
|
||||
this.surname = surname;
|
||||
}
|
||||
}
|
||||
// #simplest-custom-serializer-model
|
||||
|
||||
public
|
||||
// #simplest-custom-serializer
|
||||
/**
|
||||
* Simplest possible serializer, uses a string representation of the Person class.
|
||||
*
|
||||
* <p>Usually a serializer like this would use a library like: protobuf, kryo, avro, cap'n
|
||||
* proto, flatbuffers, SBE or some other dedicated serializer backend to perform the actual
|
||||
* to/from bytes marshalling.
|
||||
*/
|
||||
static class SimplestPossiblePersonSerializer extends SerializerWithStringManifest {
|
||||
private final Charset utf8 = StandardCharsets.UTF_8;
|
||||
|
||||
private final String personManifest = Person.class.getName();
|
||||
|
||||
// unique identifier of the serializer
|
||||
@Override
|
||||
public int identifier() {
|
||||
return 1234567;
|
||||
}
|
||||
|
||||
// extract manifest to be stored together with serialized object
|
||||
@Override
|
||||
public String manifest(Object o) {
|
||||
return o.getClass().getName();
|
||||
}
|
||||
|
||||
// serialize the object
|
||||
@Override
|
||||
public byte[] toBinary(Object obj) {
|
||||
if (obj instanceof Person) {
|
||||
Person p = (Person) obj;
|
||||
return (p.name + "|" + p.surname).getBytes(utf8);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Unable to serialize to bytes, clazz was: " + obj.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
// deserialize the object, using the manifest to indicate which logic to apply
|
||||
@Override
|
||||
public Object fromBinary(byte[] bytes, String manifest) throws NotSerializableException {
|
||||
if (personManifest.equals(manifest)) {
|
||||
String nameAndSurname = new String(bytes, utf8);
|
||||
String[] parts = nameAndSurname.split("[|]");
|
||||
return new Person(parts[0], parts[1]);
|
||||
} else {
|
||||
throw new NotSerializableException(
|
||||
"Unable to deserialize from bytes, manifest was: "
|
||||
+ manifest
|
||||
+ "! Bytes length: "
|
||||
+ bytes.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
// #simplest-custom-serializer
|
||||
}
|
||||
|
||||
public static class SamplePayload {
|
||||
private final Object payload;
|
||||
|
||||
public SamplePayload(Object payload) {
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public Object getPayload() {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
// #split-events-during-recovery
|
||||
interface Version1 {};
|
||||
|
||||
interface Version2 {}
|
||||
|
||||
// #split-events-during-recovery
|
||||
public
|
||||
// #split-events-during-recovery
|
||||
// V1 event:
|
||||
static class UserDetailsChanged implements Version1 {
|
||||
public final String name;
|
||||
public final String address;
|
||||
|
||||
public UserDetailsChanged(String name, String address) {
|
||||
this.name = name;
|
||||
this.address = address;
|
||||
}
|
||||
}
|
||||
|
||||
// #split-events-during-recovery
|
||||
public
|
||||
// #split-events-during-recovery
|
||||
// corresponding V2 events:
|
||||
static class UserNameChanged implements Version2 {
|
||||
public final String name;
|
||||
|
||||
public UserNameChanged(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
// #split-events-during-recovery
|
||||
public
|
||||
// #split-events-during-recovery
|
||||
static class UserAddressChanged implements Version2 {
|
||||
public final String address;
|
||||
|
||||
public UserAddressChanged(String address) {
|
||||
this.address = address;
|
||||
}
|
||||
}
|
||||
|
||||
// #split-events-during-recovery
|
||||
public
|
||||
// #split-events-during-recovery
|
||||
// event splitting adapter:
|
||||
static class UserEventsAdapter implements EventAdapter {
|
||||
@Override
|
||||
public String manifest(Object event) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventSeq fromJournal(Object event, String manifest) {
|
||||
if (event instanceof UserDetailsChanged) {
|
||||
UserDetailsChanged c = (UserDetailsChanged) event;
|
||||
if (c.name == null) return EventSeq.single(new UserAddressChanged(c.address));
|
||||
else if (c.address == null) return EventSeq.single(new UserNameChanged(c.name));
|
||||
else return EventSeq.create(new UserNameChanged(c.name), new UserAddressChanged(c.address));
|
||||
} else {
|
||||
return EventSeq.single(event);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object toJournal(Object event) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
// #split-events-during-recovery
|
||||
|
||||
public static class CustomerBlinked {
|
||||
public final long customerId;
|
||||
|
||||
public CustomerBlinked(long customerId) {
|
||||
this.customerId = customerId;
|
||||
}
|
||||
}
|
||||
|
||||
public
|
||||
// #string-serializer-skip-deleved-event-by-manifest
|
||||
static class EventDeserializationSkipped {
|
||||
public static EventDeserializationSkipped instance = new EventDeserializationSkipped();
|
||||
|
||||
private EventDeserializationSkipped() {}
|
||||
}
|
||||
|
||||
// #string-serializer-skip-deleved-event-by-manifest
|
||||
public
|
||||
// #string-serializer-skip-deleved-event-by-manifest
|
||||
static class RemovedEventsAwareSerializer extends SerializerWithStringManifest {
|
||||
private final Charset utf8 = StandardCharsets.UTF_8;
|
||||
private final String customerBlinkedManifest = "blinked";
|
||||
|
||||
// unique identifier of the serializer
|
||||
@Override
|
||||
public int identifier() {
|
||||
return 8337;
|
||||
}
|
||||
|
||||
// extract manifest to be stored together with serialized object
|
||||
@Override
|
||||
public String manifest(Object o) {
|
||||
if (o instanceof CustomerBlinked) return customerBlinkedManifest;
|
||||
else return o.getClass().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBinary(Object o) {
|
||||
return o.toString().getBytes(utf8); // example serialization
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromBinary(byte[] bytes, String manifest) {
|
||||
if (customerBlinkedManifest.equals(manifest)) return EventDeserializationSkipped.instance;
|
||||
else return new String(bytes, utf8);
|
||||
}
|
||||
}
|
||||
// #string-serializer-skip-deleved-event-by-manifest
|
||||
|
||||
public
|
||||
// #string-serializer-skip-deleved-event-by-manifest-adapter
|
||||
static class SkippedEventsAwareAdapter implements EventAdapter {
|
||||
@Override
|
||||
public String manifest(Object event) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object toJournal(Object event) {
|
||||
return event;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventSeq fromJournal(Object event, String manifest) {
|
||||
if (event == EventDeserializationSkipped.instance) return EventSeq.empty();
|
||||
else return EventSeq.single(event);
|
||||
}
|
||||
}
|
||||
// #string-serializer-skip-deleved-event-by-manifest-adapter
|
||||
|
||||
// #string-serializer-handle-rename
|
||||
public
|
||||
// #string-serializer-handle-rename
|
||||
static class RenamedEventAwareSerializer extends SerializerWithStringManifest {
|
||||
private final Charset utf8 = StandardCharsets.UTF_8;
|
||||
|
||||
// unique identifier of the serializer
|
||||
@Override
|
||||
public int identifier() {
|
||||
return 8337;
|
||||
}
|
||||
|
||||
private final String oldPayloadClassName =
|
||||
"docs.persistence.OldPayload"; // class NOT available anymore
|
||||
private final String myPayloadClassName = SamplePayload.class.getName();
|
||||
|
||||
// extract manifest to be stored together with serialized object
|
||||
@Override
|
||||
public String manifest(Object o) {
|
||||
return o.getClass().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBinary(Object o) {
|
||||
if (o instanceof SamplePayload) {
|
||||
SamplePayload s = (SamplePayload) o;
|
||||
return s.payload.toString().getBytes(utf8);
|
||||
} else {
|
||||
// previously also handled "old" events here.
|
||||
throw new IllegalArgumentException(
|
||||
"Unable to serialize to bytes, clazz was: " + o.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromBinary(byte[] bytes, String manifest) throws NotSerializableException {
|
||||
if (oldPayloadClassName.equals(manifest)) return new SamplePayload(new String(bytes, utf8));
|
||||
else if (myPayloadClassName.equals(manifest))
|
||||
return new SamplePayload(new String(bytes, utf8));
|
||||
else throw new NotSerializableException("unexpected manifest [" + manifest + "]");
|
||||
}
|
||||
}
|
||||
// #string-serializer-handle-rename
|
||||
|
||||
public
|
||||
// #detach-models
|
||||
// Domain model - highly optimised for domain language and maybe "fluent" usage
|
||||
static class Customer {
|
||||
public final String name;
|
||||
|
||||
public Customer(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// #detach-models
|
||||
public
|
||||
// #detach-models
|
||||
static class Seat {
|
||||
public final String code;
|
||||
|
||||
public Seat(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public SeatBooked bookFor(Customer customer) {
|
||||
return new SeatBooked(code, customer);
|
||||
}
|
||||
}
|
||||
|
||||
// #detach-models
|
||||
public
|
||||
// #detach-models
|
||||
static class SeatBooked {
|
||||
public final String code;
|
||||
public final Customer customer;
|
||||
|
||||
public SeatBooked(String code, Customer customer) {
|
||||
this.code = code;
|
||||
this.customer = customer;
|
||||
}
|
||||
}
|
||||
|
||||
// #detach-models
|
||||
public
|
||||
// #detach-models
|
||||
// Data model - highly optimised for schema evolution and persistence
|
||||
static class SeatBookedData {
|
||||
public final String code;
|
||||
public final String customerName;
|
||||
|
||||
public SeatBookedData(String code, String customerName) {
|
||||
this.code = code;
|
||||
this.customerName = customerName;
|
||||
}
|
||||
}
|
||||
// #detach-models
|
||||
|
||||
// #detach-models-adapter
|
||||
class DetachedModelsAdapter implements EventAdapter {
|
||||
@Override
|
||||
public String manifest(Object event) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object toJournal(Object event) {
|
||||
if (event instanceof SeatBooked) {
|
||||
SeatBooked s = (SeatBooked) event;
|
||||
return new SeatBookedData(s.code, s.customer.name);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported: " + event.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventSeq fromJournal(Object event, String manifest) {
|
||||
if (event instanceof SeatBookedData) {
|
||||
SeatBookedData d = (SeatBookedData) event;
|
||||
return EventSeq.single(new SeatBooked(d.code, new Customer(d.customerName)));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported: " + event.getClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
// #detach-models-adapter
|
||||
|
||||
public
|
||||
// #detach-models-adapter-json
|
||||
static class JsonDataModelAdapter implements EventAdapter {
|
||||
|
||||
// use your favorite json library
|
||||
private final ExampleJsonMarshaller marshaller = new ExampleJsonMarshaller();
|
||||
|
||||
@Override
|
||||
public String manifest(Object event) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsObject toJournal(Object event) {
|
||||
return marshaller.toJson(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventSeq fromJournal(Object event, String manifest) {
|
||||
if (event instanceof JsObject) {
|
||||
JsObject json = (JsObject) event;
|
||||
return EventSeq.single(marshaller.fromJson(json));
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Unable to fromJournal a non-JSON object! Was: " + event.getClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
// #detach-models-adapter-json
|
||||
|
||||
}
|
||||
134
docs/src/test/java/jdocs/persistence/PersistentActorExample.java
Normal file
134
docs/src/test/java/jdocs/persistence/PersistentActorExample.java
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.persistence;
|
||||
|
||||
// #persistent-actor-example
|
||||
|
||||
import org.apache.pekko.actor.ActorRef;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.actor.Props;
|
||||
import org.apache.pekko.persistence.AbstractPersistentActor;
|
||||
import org.apache.pekko.persistence.SnapshotOffer;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
|
||||
class Cmd implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final String data;
|
||||
|
||||
public Cmd(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class Evt implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final String data;
|
||||
|
||||
public Evt(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class ExampleState implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final ArrayList<String> events;
|
||||
|
||||
public ExampleState() {
|
||||
this(new ArrayList<>());
|
||||
}
|
||||
|
||||
public ExampleState(ArrayList<String> events) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
public ExampleState copy() {
|
||||
return new ExampleState(new ArrayList<>(events));
|
||||
}
|
||||
|
||||
public void update(Evt evt) {
|
||||
events.add(evt.getData());
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return events.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return events.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class ExamplePersistentActor extends AbstractPersistentActor {
|
||||
|
||||
private ExampleState state = new ExampleState();
|
||||
private int snapShotInterval = 1000;
|
||||
|
||||
public int getNumEvents() {
|
||||
return state.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String persistenceId() {
|
||||
return "sample-id-1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceiveRecover() {
|
||||
return receiveBuilder()
|
||||
.match(Evt.class, state::update)
|
||||
.match(SnapshotOffer.class, ss -> state = (ExampleState) ss.snapshot())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive createReceive() {
|
||||
return receiveBuilder()
|
||||
.match(
|
||||
Cmd.class,
|
||||
c -> {
|
||||
final String data = c.getData();
|
||||
final Evt evt = new Evt(data + "-" + getNumEvents());
|
||||
persist(
|
||||
evt,
|
||||
(Evt e) -> {
|
||||
state.update(e);
|
||||
getContext().getSystem().getEventStream().publish(e);
|
||||
if (lastSequenceNr() % snapShotInterval == 0 && lastSequenceNr() != 0)
|
||||
// IMPORTANT: create a copy of snapshot because ExampleState is mutable
|
||||
saveSnapshot(state.copy());
|
||||
});
|
||||
})
|
||||
.matchEquals("print", s -> System.out.println(state))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// #persistent-actor-example
|
||||
|
||||
public class PersistentActorExample {
|
||||
public static void main(String... args) throws Exception {
|
||||
final ActorSystem system = ActorSystem.create("example");
|
||||
final ActorRef persistentActor =
|
||||
system.actorOf(Props.create(ExamplePersistentActor.class), "persistentActor-4-java8");
|
||||
persistentActor.tell(new Cmd("foo"), null);
|
||||
persistentActor.tell(new Cmd("baz"), null);
|
||||
persistentActor.tell(new Cmd("bar"), null);
|
||||
persistentActor.tell(new Cmd("buzz"), null);
|
||||
persistentActor.tell("print", null);
|
||||
|
||||
Thread.sleep(10000);
|
||||
system.terminate();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.persistence.query;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.pekko.NotUsed;
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.persistence.journal.WriteEventAdapter;
|
||||
import org.apache.pekko.persistence.journal.Tagged;
|
||||
import org.apache.pekko.persistence.query.EventEnvelope;
|
||||
import org.apache.pekko.persistence.query.Sequence;
|
||||
import org.apache.pekko.persistence.query.PersistenceQuery;
|
||||
import org.apache.pekko.persistence.query.journal.leveldb.javadsl.LeveldbReadJournal;
|
||||
import org.apache.pekko.stream.javadsl.Source;
|
||||
|
||||
public class LeveldbPersistenceQueryDocTest {
|
||||
|
||||
final ActorSystem system = ActorSystem.create();
|
||||
|
||||
public void demonstrateReadJournal() {
|
||||
// #get-read-journal
|
||||
LeveldbReadJournal queries =
|
||||
PersistenceQuery.get(system)
|
||||
.getReadJournalFor(LeveldbReadJournal.class, LeveldbReadJournal.Identifier());
|
||||
// #get-read-journal
|
||||
}
|
||||
|
||||
public void demonstrateEventsByPersistenceId() {
|
||||
// #EventsByPersistenceId
|
||||
LeveldbReadJournal queries =
|
||||
PersistenceQuery.get(system)
|
||||
.getReadJournalFor(LeveldbReadJournal.class, LeveldbReadJournal.Identifier());
|
||||
|
||||
Source<EventEnvelope, NotUsed> source =
|
||||
queries.eventsByPersistenceId("some-persistence-id", 0, Long.MAX_VALUE);
|
||||
// #EventsByPersistenceId
|
||||
}
|
||||
|
||||
public void demonstrateAllPersistenceIds() {
|
||||
// #AllPersistenceIds
|
||||
LeveldbReadJournal queries =
|
||||
PersistenceQuery.get(system)
|
||||
.getReadJournalFor(LeveldbReadJournal.class, LeveldbReadJournal.Identifier());
|
||||
|
||||
Source<String, NotUsed> source = queries.persistenceIds();
|
||||
// #AllPersistenceIds
|
||||
}
|
||||
|
||||
public void demonstrateEventsByTag() {
|
||||
// #EventsByTag
|
||||
LeveldbReadJournal queries =
|
||||
PersistenceQuery.get(system)
|
||||
.getReadJournalFor(LeveldbReadJournal.class, LeveldbReadJournal.Identifier());
|
||||
|
||||
Source<EventEnvelope, NotUsed> source = queries.eventsByTag("green", new Sequence(0L));
|
||||
// #EventsByTag
|
||||
}
|
||||
|
||||
public
|
||||
// #tagger
|
||||
static class MyTaggingEventAdapter implements WriteEventAdapter {
|
||||
|
||||
@Override
|
||||
public Object toJournal(Object event) {
|
||||
if (event instanceof String) {
|
||||
String s = (String) event;
|
||||
Set<String> tags = new HashSet<String>();
|
||||
if (s.contains("green")) tags.add("green");
|
||||
if (s.contains("black")) tags.add("black");
|
||||
if (s.contains("blue")) tags.add("blue");
|
||||
if (tags.isEmpty()) return event;
|
||||
else return new Tagged(event, tags);
|
||||
} else {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String manifest(Object event) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
// #tagger
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright (C) 2019-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.persistence.query;
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem;
|
||||
import org.apache.pekko.persistence.query.EventEnvelope;
|
||||
import org.apache.pekko.persistence.query.Offset;
|
||||
import org.apache.pekko.serialization.Serialization;
|
||||
import org.apache.pekko.serialization.SerializationExtension;
|
||||
import org.apache.pekko.stream.*;
|
||||
import org.apache.pekko.stream.stage.*;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
// #events-by-tag-publisher
|
||||
public class MyEventsByTagSource extends GraphStage<SourceShape<EventEnvelope>> {
|
||||
public Outlet<EventEnvelope> out = Outlet.create("MyEventByTagSource.out");
|
||||
private static final String QUERY =
|
||||
"SELECT id, persistence_id, seq_nr, serializer_id, serializer_manifest, payload "
|
||||
+ "FROM journal WHERE tag = ? AND id > ? "
|
||||
+ "ORDER BY id LIMIT ?";
|
||||
|
||||
enum Continue {
|
||||
INSTANCE;
|
||||
}
|
||||
|
||||
private static final int LIMIT = 1000;
|
||||
private final Connection connection;
|
||||
private final String tag;
|
||||
private final long initialOffset;
|
||||
private final Duration refreshInterval;
|
||||
|
||||
// assumes a shared connection, could also be a factory for creating connections/pool
|
||||
public MyEventsByTagSource(
|
||||
Connection connection, String tag, long initialOffset, Duration refreshInterval) {
|
||||
this.connection = connection;
|
||||
this.tag = tag;
|
||||
this.initialOffset = initialOffset;
|
||||
this.refreshInterval = refreshInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Attributes initialAttributes() {
|
||||
return Attributes.apply(ActorAttributes.IODispatcher());
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceShape<EventEnvelope> shape() {
|
||||
return SourceShape.of(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphStageLogic createLogic(Attributes inheritedAttributes) {
|
||||
return new TimerGraphStageLogic(shape()) {
|
||||
private ActorSystem system = materializer().system();
|
||||
private long currentOffset = initialOffset;
|
||||
private List<EventEnvelope> buf = new LinkedList<>();
|
||||
private final Serialization serialization = SerializationExtension.get(system);
|
||||
|
||||
@Override
|
||||
public void preStart() {
|
||||
scheduleWithFixedDelay(Continue.INSTANCE, refreshInterval, refreshInterval);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimer(Object timerKey) {
|
||||
query();
|
||||
deliver();
|
||||
}
|
||||
|
||||
private void deliver() {
|
||||
if (isAvailable(out) && !buf.isEmpty()) {
|
||||
push(out, buf.remove(0));
|
||||
}
|
||||
}
|
||||
|
||||
private void query() {
|
||||
if (buf.isEmpty()) {
|
||||
|
||||
try (PreparedStatement s = connection.prepareStatement(QUERY)) {
|
||||
s.setString(1, tag);
|
||||
s.setLong(2, currentOffset);
|
||||
s.setLong(3, LIMIT);
|
||||
try (ResultSet rs = s.executeQuery()) {
|
||||
final List<EventEnvelope> res = new ArrayList<>(LIMIT);
|
||||
while (rs.next()) {
|
||||
Object deserialized =
|
||||
serialization
|
||||
.deserialize(
|
||||
rs.getBytes("payload"),
|
||||
rs.getInt("serializer_id"),
|
||||
rs.getString("serializer_manifest"))
|
||||
.get();
|
||||
currentOffset = rs.getLong("id");
|
||||
res.add(
|
||||
new EventEnvelope(
|
||||
Offset.sequence(currentOffset),
|
||||
rs.getString("persistence_id"),
|
||||
rs.getLong("seq_nr"),
|
||||
deserialized,
|
||||
System.currentTimeMillis()));
|
||||
}
|
||||
buf = res;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
failStage(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
setHandler(
|
||||
out,
|
||||
new AbstractOutHandler() {
|
||||
@Override
|
||||
public void onPull() {
|
||||
query();
|
||||
deliver();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
// #events-by-tag-publisher
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (C) 2019-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.persistence.testkit;
|
||||
|
||||
import org.apache.pekko.actor.typed.ActorSystem;
|
||||
import org.apache.pekko.actor.typed.Behavior;
|
||||
import org.apache.pekko.persistence.testkit.PersistenceTestKitPlugin;
|
||||
import org.apache.pekko.persistence.testkit.PersistenceTestKitSnapshotPlugin;
|
||||
import org.apache.pekko.persistence.testkit.javadsl.PersistenceTestKit;
|
||||
import org.apache.pekko.persistence.testkit.javadsl.SnapshotTestKit;
|
||||
import com.typesafe.config.Config;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
|
||||
public class Configuration {
|
||||
|
||||
// #testkit-typed-conf
|
||||
public class PersistenceTestKitConfig {
|
||||
|
||||
Config conf =
|
||||
PersistenceTestKitPlugin.getInstance()
|
||||
.config()
|
||||
.withFallback(ConfigFactory.defaultApplication());
|
||||
|
||||
ActorSystem<Command> system = ActorSystem.create(new SomeBehavior(), "example", conf);
|
||||
|
||||
PersistenceTestKit testKit = PersistenceTestKit.create(system);
|
||||
}
|
||||
// #testkit-typed-conf
|
||||
|
||||
// #snapshot-typed-conf
|
||||
public class SnapshotTestKitConfig {
|
||||
|
||||
Config conf =
|
||||
PersistenceTestKitSnapshotPlugin.getInstance()
|
||||
.config()
|
||||
.withFallback(ConfigFactory.defaultApplication());
|
||||
|
||||
ActorSystem<Command> system = ActorSystem.create(new SomeBehavior(), "example", conf);
|
||||
|
||||
SnapshotTestKit testKit = SnapshotTestKit.create(system);
|
||||
}
|
||||
// #snapshot-typed-conf
|
||||
|
||||
}
|
||||
|
||||
class SomeBehavior extends Behavior<Command> {
|
||||
public SomeBehavior() {
|
||||
super(1);
|
||||
}
|
||||
}
|
||||
|
||||
class Command {}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.persistence.testkit;
|
||||
|
||||
import org.apache.pekko.actor.testkit.typed.javadsl.TestKitJunitResource;
|
||||
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
// #imports
|
||||
import org.apache.pekko.persistence.testkit.javadsl.PersistenceInit;
|
||||
import org.apache.pekko.Done;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
// #imports
|
||||
|
||||
public class PersistenceInitTest extends AbstractJavaTest {
|
||||
@ClassRule
|
||||
public static final TestKitJunitResource testKit =
|
||||
new TestKitJunitResource(
|
||||
ConfigFactory.parseString(
|
||||
"akka.persistence.journal.plugin = \"akka.persistence.journal.inmem\" \n"
|
||||
+ "akka.persistence.journal.inmem.test-serialization = on \n"
|
||||
+ "akka.persistence.snapshot-store.plugin = \"akka.persistence.snapshot-store.local\" \n"
|
||||
+ "akka.persistence.snapshot-store.local.dir = \"target/snapshot-"
|
||||
+ UUID.randomUUID().toString()
|
||||
+ "\" \n")
|
||||
.withFallback(ConfigFactory.defaultApplication()));
|
||||
|
||||
@Test
|
||||
public void testInit() throws Exception {
|
||||
// #init
|
||||
Duration timeout = Duration.ofSeconds(5);
|
||||
CompletionStage<Done> done =
|
||||
PersistenceInit.initializeDefaultPlugins(testKit.system(), timeout);
|
||||
done.toCompletableFuture().get(timeout.getSeconds(), TimeUnit.SECONDS);
|
||||
// #init
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (C) 2020-2022 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.persistence.testkit;
|
||||
|
||||
import org.apache.pekko.actor.testkit.typed.javadsl.TestKitJunitResource;
|
||||
import org.apache.pekko.actor.typed.ActorRef;
|
||||
import org.apache.pekko.persistence.testkit.JournalOperation;
|
||||
import org.apache.pekko.persistence.testkit.PersistenceTestKitPlugin;
|
||||
import org.apache.pekko.persistence.testkit.ProcessingPolicy;
|
||||
import org.apache.pekko.persistence.testkit.ProcessingResult;
|
||||
import org.apache.pekko.persistence.testkit.ProcessingSuccess;
|
||||
import org.apache.pekko.persistence.testkit.StorageFailure;
|
||||
import org.apache.pekko.persistence.testkit.WriteEvents;
|
||||
import org.apache.pekko.persistence.testkit.javadsl.PersistenceTestKit;
|
||||
import org.apache.pekko.persistence.typed.PersistenceId;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
import jdocs.AbstractJavaTest;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
// #policy-test
|
||||
public class PersistenceTestKitPolicySampleTest extends AbstractJavaTest {
|
||||
|
||||
@ClassRule
|
||||
public static final TestKitJunitResource testKit =
|
||||
new TestKitJunitResource(
|
||||
PersistenceTestKitPlugin.getInstance()
|
||||
.config()
|
||||
.withFallback(ConfigFactory.defaultApplication()));
|
||||
|
||||
PersistenceTestKit persistenceTestKit = PersistenceTestKit.create(testKit.system());
|
||||
|
||||
@Before
|
||||
public void beforeEach() {
|
||||
persistenceTestKit.clearAll();
|
||||
persistenceTestKit.resetPolicy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
SampleEventStoragePolicy policy = new SampleEventStoragePolicy();
|
||||
persistenceTestKit.withPolicy(policy);
|
||||
|
||||
PersistenceId persistenceId = PersistenceId.ofUniqueId("some-id");
|
||||
ActorRef<YourPersistentBehavior.Cmd> ref =
|
||||
testKit.spawn(YourPersistentBehavior.create(persistenceId));
|
||||
|
||||
YourPersistentBehavior.Cmd cmd = new YourPersistentBehavior.Cmd("data");
|
||||
ref.tell(cmd);
|
||||
|
||||
persistenceTestKit.expectNothingPersisted(persistenceId.id());
|
||||
}
|
||||
|
||||
static class SampleEventStoragePolicy implements ProcessingPolicy<JournalOperation> {
|
||||
@Override
|
||||
public ProcessingResult tryProcess(String processId, JournalOperation processingUnit) {
|
||||
if (processingUnit instanceof WriteEvents) {
|
||||
return StorageFailure.create();
|
||||
} else {
|
||||
return ProcessingSuccess.getInstance();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// #policy-test
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue