Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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)