package lecture13 import concurrent.{ExecutionContext, Future, Promise} import concurrent.duration.DurationInt import akka.actor.{Actor, ActorContext, ActorRef, ActorSystem, Props} import akka.event.LoggingReceive import akka.pattern.pipe import akka.testkit.{ImplicitSender, TestKit} import akka.util.Timeout /** * This demonstrates a simplified implementation of the ask pattern. * * @param promise the promise to be completed when a message is received */ class AskMiniActor(promise: Promise[Any]) extends Actor: def receive = LoggingReceive { case msg => promise.success(msg) } extension (receiver: ActorRef) def ?(msg: Any)(using // In this simplified implementation, we don't use the timeout. timeout: Timeout, // This only used for logging purposes (and to get the actor system in the // real implementation), but is not used otherwise in the implementation. sender: ActorRef, // In the real implementation, the actor system is retrieved differently, // but here we pass it as an additional implicit parameter for simplicity. context: ActorContext ): Future[Any] = context.system.log.debug(s"Create mini actor to ask $msg from $sender to $receiver") // Create a `Promise` that will be completed when a message is received. val promise = Promise[Any]() // Create a mini actor that will complete the `Promise` when it receives a // message. val miniActor = context.system.actorOf(Props(AskMiniActor(promise))) // Send the message to the mini actor, *using the mini actor as the sender*. // This part is important as it is the mini actor that needs to receive the // response. receiver.tell(msg, miniActor) // Using the `Promise` from the Scala standard library, the corresponding // `Future` can be retrieved with the method `future`. promise.future object Person: enum Protocol: case GetName case GetAge enum Response: case Name(name: String) case Age(age: Int) case UnknownMessage class Person(name: String, age: Int) extends Actor: import Person.Protocol._ import Person.Response._ def receive = LoggingReceive { case GetName => sender() ! Name(name) case GetAge => sender() ! Age(age) case _ => sender() ! UnknownMessage } object PersonsDatabase: enum Protocol: case CreatePerson(name: String, age: Int) case GetPersonNames(ids: List[Int]) enum Response: case PersonCreated(id: Int) case PersonNames(names: List[String]) class PersonsDatabase extends Actor: import Person.Protocol._ import Person.Response._ import PersonsDatabase.Protocol._ import PersonsDatabase.Response._ var persons: Map[Int, ActorRef] = Map.empty var maxId = 0 given ExecutionContext = context.system.dispatcher given Timeout = Timeout(200.millis) def receive = LoggingReceive { case CreatePerson(name, age) => val personRef = context.actorOf(Props(Person(name, age))) persons = persons + (maxId -> personRef) sender() ! PersonCreated(maxId) maxId += 1 case GetPersonNames(ids) => // We ask every person for their name using the ask pattern. This // gives us a list of `Future`s that will each be completed with a // `Name` message. val rawResponsesFutures: List[Future[Any]] = ids.map(id => (persons(id) ? GetName)) // We first map each `Future[Any]` to a `Future[Name]` using the // `mapTo` method. Then, we map each `Future[Name]` to a // `Future[String]` using the `map` method. val namesFutures: List[Future[String]] = rawResponsesFutures.map(_.mapTo[Name].map(_.name)) // We use the `Future.sequence` method to convert a // `List[Future[String]]` to a `Future[List[String]]`. The resulting // future will be completed once all the futures in the input list // are completed. val futureOfNames: Future[List[String]] = Future.sequence(namesFutures) // Finally, map the `Future[List[String]]` to a // `Future[PersonNames]` and pipe it to the sender of the // `GetPersonNames` message. futureOfNames.map(PersonNames.apply).pipeTo(sender()) } @main def askPatternDemo() = new TestKit(ActorSystem("DebugSystem")) with ImplicitSender: import Person.Protocol._ import Person.Response._ import PersonsDatabase.Protocol._ import PersonsDatabase.Response._ try val personsDb = system.actorOf(Props(PersonsDatabase())) personsDb ! CreatePerson("Anita", 20) expectMsg(PersonCreated(0)) personsDb ! CreatePerson("Berta", 30) expectMsg(PersonCreated(1)) personsDb ! CreatePerson("Cecilia", 40) expectMsg(PersonCreated(2)) personsDb ! GetPersonNames(List(0, 1, 2)) expectMsg(PersonNames(List("Anita", "Berta", "Cecilia"))) finally shutdown(system)