diff --git a/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/InteractionPatternsTest.java b/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/InteractionPatternsTest.java index bfa12bbc9d..901e17cfd5 100644 --- a/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/InteractionPatternsTest.java +++ b/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/InteractionPatternsTest.java @@ -620,25 +620,57 @@ public class InteractionPatternsTest extends JUnitSuite { interface StandaloneAskSample { // #standalone-ask - public class CookieFabric { + public class CookieFabric extends AbstractBehavior { interface Command {} public static class GiveMeCookies implements Command { - public final ActorRef cookies; + public final int count; + public final ActorRef replyTo; - public GiveMeCookies(ActorRef cookies) { - this.cookies = cookies; + public GiveMeCookies(int count, ActorRef replyTo) { + this.count = count; + this.replyTo = replyTo; } } - public static class Cookies { + interface Reply {} + + public static class Cookies implements Reply { public final int count; public Cookies(int count) { this.count = count; } } + + public static class InvalidRequest implements Reply { + public final String reason; + + public InvalidRequest(String reason) { + this.reason = reason; + } + } + + public static Behavior create() { + return Behaviors.setup(CookieFabric::new); + } + + private CookieFabric(ActorContext context) { + super(context); + } + + @Override + public Receive createReceive() { + return newReceiveBuilder().onMessage(GiveMeCookies.class, this::onGiveMeCookies).build(); + } + + private Behavior onGiveMeCookies(GiveMeCookies request) { + if (request.count >= 5) request.replyTo.tell(new InvalidRequest("Too many cookies.")); + else request.replyTo.tell(new Cookies(request.count)); + + return this; + } } // #standalone-ask @@ -648,23 +680,59 @@ public class InteractionPatternsTest extends JUnitSuite { public void askAndPrint( ActorSystem system, ActorRef cookieFabric) { - CompletionStage result = + CompletionStage result = AskPattern.ask( cookieFabric, - CookieFabric.GiveMeCookies::new, + replyTo -> new CookieFabric.GiveMeCookies(3, replyTo), // asking someone requires a timeout and a scheduler, if the timeout hits without - // response - // the ask is failed with a TimeoutException + // response the ask is failed with a TimeoutException Duration.ofSeconds(3), system.scheduler()); result.whenComplete( - (cookies, failure) -> { - if (cookies != null) System.out.println("Yay, cookies!"); - else System.out.println("Boo! didn't get cookies in time."); + (reply, failure) -> { + if (reply instanceof CookieFabric.Cookies) + System.out.println("Yay, " + ((CookieFabric.Cookies) reply).count + " cookies!"); + else if (reply instanceof CookieFabric.InvalidRequest) + System.out.println( + "No cookies for me. " + ((CookieFabric.InvalidRequest) reply).reason); + else System.out.println("Boo! didn't get cookies in time. " + failure); }); } // #standalone-ask + + public void askAndMapInvalid( + ActorSystem system, ActorRef cookieFabric) { + // #standalone-ask-fail-future + CompletionStage result = + AskPattern.ask( + cookieFabric, + replyTo -> new CookieFabric.GiveMeCookies(3, replyTo), + Duration.ofSeconds(3), + system.scheduler()); + + CompletionStage cookies = + result.thenCompose( + (CookieFabric.Reply reply) -> { + if (reply instanceof CookieFabric.Cookies) { + return CompletableFuture.completedFuture((CookieFabric.Cookies) reply); + } else if (reply instanceof CookieFabric.InvalidRequest) { + CompletableFuture failed = new CompletableFuture<>(); + failed.completeExceptionally( + new IllegalArgumentException(((CookieFabric.InvalidRequest) reply).reason)); + return failed; + } else { + throw new IllegalStateException("Unexpected reply: " + reply.getClass()); + } + }); + + cookies.whenComplete( + (cookiesReply, failure) -> { + if (cookies != null) System.out.println("Yay, " + cookiesReply.count + " cookies!"); + else System.out.println("Boo! didn't get cookies in time. " + failure); + }); + // #standalone-ask-fail-future + } } } diff --git a/akka-actor-typed-tests/src/test/scala/docs/akka/typed/InteractionPatternsSpec.scala b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/InteractionPatternsSpec.scala index 75d3aefb0f..225e0d7cb4 100644 --- a/akka-actor-typed-tests/src/test/scala/docs/akka/typed/InteractionPatternsSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/InteractionPatternsSpec.scala @@ -391,12 +391,18 @@ class InteractionPatternsSpec extends ScalaTestWithActorTestKit with WordSpecLik // #standalone-ask object CookieFabric { sealed trait Command {} - case class GiveMeCookies(replyTo: ActorRef[Cookies]) extends Command - case class Cookies(count: Int) + case class GiveMeCookies(count: Int, replyTo: ActorRef[Reply]) extends Command + + sealed trait Reply + case class Cookies(count: Int) extends Reply + case class InvalidRequest(reason: String) extends Reply def apply(): Behaviors.Receive[CookieFabric.GiveMeCookies] = Behaviors.receiveMessage { message => - message.replyTo ! Cookies(5) + if (message.count >= 5) + message.replyTo ! InvalidRequest("Too many cookies.") + else + message.replyTo ! Cookies(message.count) Behaviors.same } } @@ -414,18 +420,34 @@ class InteractionPatternsSpec extends ScalaTestWithActorTestKit with WordSpecLik // the ask is failed with a TimeoutException implicit val timeout: Timeout = 3.seconds - val result: Future[CookieFabric.Cookies] = cookieFabric.ask(ref => CookieFabric.GiveMeCookies(ref)) + val result: Future[CookieFabric.Reply] = cookieFabric.ask(ref => CookieFabric.GiveMeCookies(3, ref)) // the response callback will be executed on this execution context implicit val ec = system.executionContext result.onComplete { - case Success(cookies) => println(s"Yay, cookies! $cookies") - case Failure(ex) => println(s"Boo! didn't get cookies: ${ex.getMessage}") + case Success(CookieFabric.Cookies(count)) => println(s"Yay, $count cookies!") + case Success(CookieFabric.InvalidRequest(reason)) => println(s"No cookies for me. $reason") + case Failure(ex) => println(s"Boo! didn't get cookies: ${ex.getMessage}") } // #standalone-ask - result.futureValue shouldEqual CookieFabric.Cookies(5) + result.futureValue shouldEqual CookieFabric.Cookies(3) + + // #standalone-ask-fail-future + val cookies: Future[CookieFabric.Cookies] = + cookieFabric.ask[CookieFabric.Reply](ref => CookieFabric.GiveMeCookies(3, ref)).flatMap { + case c: CookieFabric.Cookies => Future.successful(c) + case CookieFabric.InvalidRequest(reason) => Future.failed(new IllegalArgumentException(reason)) + } + + cookies.onComplete { + case Success(CookieFabric.Cookies(count)) => println(s"Yay, $count cookies!") + case Failure(ex) => println(s"Boo! didn't get cookies: ${ex.getMessage}") + } + // #standalone-ask-fail-future + + cookies.futureValue shouldEqual CookieFabric.Cookies(3) } "contain a sample for pipeToSelf" in { diff --git a/akka-docs/src/main/paradox/typed/interaction-patterns.md b/akka-docs/src/main/paradox/typed/interaction-patterns.md index b5952f1c13..e6c5d2b7d1 100644 --- a/akka-docs/src/main/paradox/typed/interaction-patterns.md +++ b/akka-docs/src/main/paradox/typed/interaction-patterns.md @@ -206,6 +206,17 @@ Scala Java : @@snip [InteractionPatternsTest.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/InteractionPatternsTest.java) { #standalone-ask } +Note that validation errors are also explicit in the message protocol. The `GiveMeCookies` request can reply +with `Cookies` or `InvalidRequest`. The requestor has to decide how to handle `InvalidRequest` reply. Sometimes +that should be treated as a failed @scala[`Future`]@java[`Future`] and for that the reply can be mapped on the +requestor side. + +Scala +: @@snip [InteractionPatternsSpec.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/InteractionPatternsSpec.scala) { #standalone-ask-fail-future } + +Java +: @@snip [InteractionPatternsTest.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/InteractionPatternsTest.java) { #standalone-ask-fail-future } + **Useful when:** * Querying an actor from outside of the actor system