Skip to content
Snippets Groups Projects
AskPattern.scala 4.78 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

Matt Bovel's avatar
Matt Bovel committed
/** This demonstrates a simplified implementation of the ask pattern.
Matt Bovel's avatar
Matt Bovel committed
  *
Matt Bovel's avatar
Matt Bovel committed
  * @param promise
  *   the promise to be completed when a message is received
Matt Bovel's avatar
Matt Bovel committed
  */
class AskMiniActor(promise: Promise[Any]) extends Actor:
  def receive = LoggingReceive {
    case msg => promise.success(msg)
  }

extension (receiver: ActorRef)
  def ?(msg: Any)(using
Matt Bovel's avatar
Matt Bovel committed
      // 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
Matt Bovel's avatar
Matt Bovel committed
  ): Future[Any] =
Matt Bovel's avatar
Matt Bovel committed
    context.system.log.debug(
      s"Create mini actor to ask $msg from $sender to $receiver"
    )
Matt Bovel's avatar
Matt Bovel committed

    // 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:
Matt Bovel's avatar
Matt Bovel committed
  enum Protocol:
    case GetName
    case GetAge
  enum Response:
    case Name(name: String)
    case Age(age: Int)
    case UnknownMessage
Matt Bovel's avatar
Matt Bovel committed

class Person(name: String, age: Int) extends Actor:
Matt Bovel's avatar
Matt Bovel committed
  import Person.Protocol.*
  import Person.Response.*
Matt Bovel's avatar
Matt Bovel committed

  def receive = LoggingReceive {
    case GetName => sender() ! Name(name)
Matt Bovel's avatar
Matt Bovel committed
    case GetAge  => sender() ! Age(age)
    case _       => sender() ! UnknownMessage
Matt Bovel's avatar
Matt Bovel committed
  }

object PersonsDatabase:
Matt Bovel's avatar
Matt Bovel committed
  enum Protocol:
    case CreatePerson(name: String, age: Int)
    case GetPersonNames(ids: List[Int])
  enum Response:
    case PersonCreated(id: Int)
    case PersonNames(names: List[String])
Matt Bovel's avatar
Matt Bovel committed

class PersonsDatabase extends Actor:
Matt Bovel's avatar
Matt Bovel committed
  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)

Matt Bovel's avatar
Matt Bovel committed
      // Finally, map the `Future[List[String]]` to a `Future[PersonNames]` and
      // pipe it to the sender of the `GetPersonNames` message.
Matt Bovel's avatar
Matt Bovel committed
      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)