Skip to content
Snippets Groups Projects
AskPattern.scala 4.9 KiB
Newer Older
Matt Bovel's avatar
Matt Bovel committed
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)