A Cameo That Is Worth an Oscar
Proper context handling is essential when working with Akka actors. Let's see the Cameo design pattern in action and see how it solves some complex problems.
Join the DZone community and get the full member experience.
Join For Freerarely during my life as a developer have i found pre-packaged solutions that fit my problem perfectly. design patterns are an abstraction of both problems and solutions. so, they often need some kind of customization on the specific problem. while i was developing my concrete instance of the actorbase specification , i came across the cameo pattern . it enlighted my way and my vision about how to use actors profitably. let’s see how and why.
the problem: capturing context
jamie allen, in his short but worthwhile book effective akka , begins the chapter dedicated to actor patterns with the following words:
one of the most difficult tasks in asynchronous programming is trying to capture context so that the state of the world at the time the task was started can be accurately represented at the time the task finishes.
this is exactly the problem we are going to try to resolve.
actors often model long-lived asynchronous processes in which a response in the future corresponds to one or more messages sent earlier. meanwhile, the context of execution of the actor could be changed. in the case of an actor, its context is represented by all the mutable variables owned by the actor itself. a notable example is the
sender
variable that stores the sender of the current message being processed by an actor.
context handling in actorbase actors
let’s make a concrete example. in actorbase, there are two types of actors among the others:
storefinder
and
storekeeper
. each actor of type
storefinder
represents a
distributed map
or a
collection
, but it does not physically store the key-value couples. this information is stored by
storekeeper
actors. so, each
storefinder
owns a distributed set of its key-value couples, which means that owns a set of
storekeeper
actors that stores the information for it.
storefinder
can route many types of messages to its
storekeeper
, representing crud operations on the data stored. the problem here is that if a
storefinder
owns
n
storekeepers
, to
find
which value corresponds to a
key
(if any), it has to send
n
messages of type
get("key")
to each
storekeeper
. once all the
storekeepers
answer the query messages, the
storefinder
can answer to its caller with the requested
value
.
the sequence diagram below depicts exactly the above scenario.
the number of answers of
storekeeper
actors and the body of their responses represent the execution context of a
storefinder
actor.
actor’s context handling
so, we need to identify a concrete method to handle the execution context of an actor. the problem is that between the sending of a message and the time when the relative response is received, an actor processes many other messages.
naive solution
using nothing more than my ignorance, the first solution i depicted in actorbase was the following.
class storefinder(val name: string) extends actor {
def receive: receive = nonemptytable(storefinderstate(map()))
def nonemptytable(state: storefinderstate): receive = {
// query messages from externl actors
case query(key, u) =>
// route a get message to each storekeeper
broadcastrouter.route(get(key, u), self)
context.become(nonemptytable(state.addquery(key, u, sender())))
// some other stuff...
// responses from storekeeper
case res:
item =>
context.become(nonemptytable(state.copy(queries = item(res, state.queries))))
}
// handling a response from a storekeeper. have they all answer? is there at least
// a storekeeper that answer with a value? how can a storefinder store the original
// sender?
private def item(response: item,
queries: map[long, queryreq]): map[long, queryreq] = {
val item(key, opt, id) = response
val queryreq(actor, responses) = queries(id)
val newresponses = opt::responses
if (newresponses.length == numberofpartitions) {
// some code to create the message
actor!queryack(key, item, id)
queries - id
} else {
queries + (id - > queryreq(actor, newresponses))
}
}
}
// i need a class to maintain the execution context
case class storefinderstate(queries: map[long, queryreq]) {
def addquery(key: string, id: long, sender: actorref): storefinderstate = {
// such a complex data structure!
copy(queries = queries + (id - > queryreq(sender, list[option[(array[byte], long)]]())))
}
// similar code for other crud operations
}
sealed
case class queryreq(sender: actorref, responses: list[option[(array[byte], long)]])
that's a lot of code to merely handle a bunch of messages, isn’t it? as you can see, to handle the execution context, i defined a dedicated class:
storefinderstate
. for each
query
message identified by a
uuid
of type
long
, this class stores the following information:
- the original sender
-
the list of responses from
storekeeper
actors for the message -
the values the
storekeeper
answered with
as you can imagine, the handling process of this context is not simple, as a single
storefinder
has to handle all the messages that have not received a final response from all the relative
storekeeper
.
we can do much better, trust me.
asking the future
a first attempt to reach a more elegant and concise solution might be the use of the
ask pattern
with
future
.
this is a great way to design your actors, in that they will not block waiting for responses, allowing them to handle more messages concurrently and increasing your application’s performance.
using the ask pattern, the code that handles the
query
message and its responses will reduce to the following.
case query(key, u) =>
val futurequeryack:
future[queryack] =
for {
responses < -future.sequence(routees map(ask(_, get(key, u))).mapto[item])
}
yield {
queryack( /* some code to create the queryack message from responses */ )
}
futurequeryack map(sender!_)
whoa! this code is fairly concise with respect to the previous snippet. in addition, using
future
and a syntax that is fairly declarative, we can achieve, quite easily, the right grade of asynchronous execution that we need.
however, there are a couple of things about it that are not ideal. first of all, it is using futures to ask other actors for responses, which creates a new
promiseactorref
for every message sent behind the scenes. this is a waste of resources.
annoying.
furthermore, there is a glaring race condition in this code — can you see it? we’re referencing the “sender” in our map operation on the result from
futurequeryack
, which may not be the sameactorref
when the future completes, because thestorefinder
actorref may now be handling another message from a different sender at that point!
even more annoying!
the problem here is that we are attempting to take the result of the off-thread operations of retrieving data from multiple sources and return it to whoever sent the original request to the
storefinder
. but the actor will likely have move onto handling additional messages in its mailbox by the time the above futures complete.
the trick is capturing the execution context of a request in a dedicated inner actor. let’s see how our code will become.
case query(key, u) => {
// capturing the original sender
val originalsender = sender
// handling the execution in a dedicated actor
context.actorof(props(new actor() {
// the list of responses from storekeepers
var responses: list[option[(array[byte], long)]] = nil
def receive = {
case item(key, opt, u) =>
responses = opt::responses
if (responses.length == partitions) {
// some code that creates the queryack message
originalsender!queryack(key, item, u)
context.stop(self)
}
}
}))
}
much better. we have captured the context for a single request to
storefinder
as the context of a dedicated actor. the original sender of the
storefinder
actor was captured by the constant
originalsender
and shared with the anonymous actor using a
closure
.
it’s easy, isn’t it? this simple trick is known as the extra pattern . however, we are searching for a cameo in our movie.
finally presenting the cameo pattern
the extra pattern is very useful when the code inside the anonymous actor is very small and trivial. otherwise, it pollutes the main actor with details that do not belong to its responsibility (one for all, actor creation).
it is also similar to lambdas, in that using an anonymous instance gives you less information in stack traces on the jvm, is harder to use with a debugging tool, and is easier to close over state.
luckily, the solution is quite easy. we can move the anonymous implementation of the actor into its own type definition.
this results in a type only used for simple interactions between actors, similar to a cameo role in the movies.
doing so, the code finally becomes the following.
class storefinder(val name: string) extends actor {
override def receive: receive = {
// omissis...
case query(key, u) =>
val originalsender = sender()
val handler = context.actorof(props(new queryresponsehandler(originalsender, numberofpartitions)))
broadcastrouter.route(get(key, u), handler)
}
// omissis...
}
// the actor playing the cameo role
class queryresponsehandler(originalsender: actorref, partitions: int) {
var responses: list[option[(array[byte], long)]] = nil
override def receive: receive = loggingreceive {
case item(key, opt, u) =>
responses = opt::responses
if (responses.length == partitions) {
// some code to make up a queryack message
originalsender!queryack(key, item, u)
context.stop(self)
}
}
}
much cleaner, so satisfying.
notice that the router in the
storefinder
tells the routees to answer to the actor that handles the query messages,
broadcastrouter.route(get(key, u), handler)
. moreover, remember to capture the
sender
in a local variable in the main actor before passing its reference to the inner actor.
make certain you follow that pattern, since passing the sender
actorref
without first capturing it will expose your handler to the same problem that we saw earlier, where the senderactorref
changed.
conclusions
so far so good. we started by stating that context handling is not so trivial when we talk about akka actors. i showed you my first solution to such a problem in actorbase, the database based on the actor model i am developing.
we agreed that we do not like it.
so, we moved on and we tried to use
future
s. the solution was elegant but suffered from race conditions. in the path through the last solution, we met the
extra pattern
, which solved the original problem without any potential drawback. the only problem is that this solution was not clean enough. finally, we approached the cameo pattern, and it shined in all its beauty.
simple
,
clean
,
elegant
.
p.s.: all the code relative to actorbase can be found on my github .
references
Published at DZone with permission of Riccardo Cardin, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
RBAC With API Gateway and Open Policy Agent (OPA)
-
Extending Java APIs: Add Missing Features Without the Hassle
-
Database Integration Tests With Spring Boot and Testcontainers
-
The SPACE Framework for Developer Productivity
Comments