/* * Copyright (C) 2009-2020 Lightbend Inc. */ package jdocs.actor; import akka.actor.*; import akka.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 akka.actor.CoordinatedShutdown; import akka.Done; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.time.Duration; import akka.testkit.TestActors; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.*; // #import-props import akka.actor.Props; // #import-props // #import-actorRef import akka.actor.ActorRef; import akka.actor.ActorSystem; // #import-actorRef // #import-identify import akka.actor.ActorIdentity; import akka.actor.ActorSelection; import akka.actor.Identify; // #import-identify // #import-ask import static akka.pattern.Patterns.ask; import static akka.pattern.Patterns.pipe; import java.util.concurrent.CompletableFuture; // #import-ask // #import-gracefulStop import static akka.pattern.Patterns.gracefulStop; import akka.pattern.AskTimeoutException; import java.util.concurrent.CompletionStage; // #import-gracefulStop // #import-terminated import akka.actor.Terminated; // #import-terminated public class ActorDocTest extends AbstractJavaTest { public static Config config = ConfigFactory.parseString( "akka {\n" + " loggers = [\"akka.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 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 akka.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 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 } @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"); } }; } 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 future1 = ask(actorA, "request", Duration.ofMillis(1000)).toCompletableFuture(); // using timeout from above CompletableFuture future2 = ask(actorB, "another request", t).toCompletableFuture(); CompletableFuture transformed = CompletableFuture.allOf(future1, future2) .thenApply( v -> { String x = (String) future1.join(); String s = (String) future2.join(); return new Result(x, 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(akka.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(akka.actor.PoisonPill.getInstance(), ActorRef.noSender()); // #poison-pill expectTerminated(Duration.ofSeconds(3), victim); } }; } private CompletionStage cleanup() { return null; } @Test public void coordinatedShutdown() { final ActorRef someActor = system.actorOf(Props.create(FirstActor.class)); // #coordinated-shutdown-addTask CoordinatedShutdown.get(system) .addTask( CoordinatedShutdown.PhaseBeforeServiceUnbind(), "someTaskName", () -> { return akka.pattern.Patterns.ask(someActor, "stop", Duration.ofSeconds(5)) .thenApply(reply -> Done.getInstance()); }); // #coordinated-shutdown-addTask // #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 CompletionStage done = CoordinatedShutdown.get(system).runAll(CoordinatedShutdown.unknownReason()); // #coordinated-shutdown-run } } @Test public void coordinatedShutdownActorTermination() { ActorRef someActor = system.actorOf(Props.create(FirstActor.class)); someActor.tell(PoisonPill.getInstance(), ActorRef.noSender()); // #coordinated-shutdown-addActorTerminationTask CoordinatedShutdown.get(system) .addActorTerminationTask( CoordinatedShutdown.PhaseBeforeServiceUnbind(), "someTaskName", someActor, Optional.of("stop")); // #coordinated-shutdown-addActorTerminationTask } }