OurFlightAttendanthas been pretty simple so far, and we’d like to change that by giving it a few more messages to process: we’ll give it some state so that it’s doing something practical.
When theFlightAttendantis currently getting a drink for a passenger, it doesn’t make much sense to get another one, considering that there are more than a few FlightAttendants available. We’ll add a couple of dif- ferent behavioural message handlers to ourFlightAttendantthat facilitate this, as well as provide a mechanism for dealing with passenger emergencies.
We begin by defining a few more messages in theFlightAttendant’s companion object:
object FlightAttendant { ...
case class Assist(passenger: ActorRef) case object Busy_?
case object Yes case object No ...
}
We’ll be using these newly defined messages in the changes we’ll make to theFlightAttendant’s message handling structure below:
class FlightAttendant extends Actor { this: AttendantResponsiveness =>
import FlightAttendant._
// bring the execution context into implicit scope for the // scheduler below
implicit val ec = context.dispatcher
// An internal message we can use to signal that drink // delivery can take place
case class DeliverDrink(drink: Drink)
// Stores our timer, which is an instance of 'Cancellable' var pendingDelivery: Option[Cancellable] = None
// Just makes scheduling a delivery a bit simpler
def scheduleDelivery(drinkname: String): Cancellable = { context.system.scheduler.scheduleOnce(responseDuration,
self,
DeliverDrink(Drink(drinkname))) }
// If we have an injured passenger, then we need to // immediately assist them, by giving them the 'secret' // Magic Healing Potion that's available on all flights in // and out of Xanadu
def assistInjuredPassenger: Receive = { case Assist(passenger) =>
// It's an emergency... stop what we're doing and // assist NOW
pendingDelivery foreach { _.cancel() } pendingDelivery = None
passenger ! Drink("Magic Healing Potion") }
// This general handler is responsible for servicing drink // requests when we're not busy servicing an existing // request
def handleDrinkRequests: Receive = { case GetDrink(drinkname) =>
pendingDelivery = Some(scheduleDelivery(drinkname)) // Become something new
context.become(assistInjuredPassenger orElse handleSpecificPerson(sender)) case Busy_? =>
sender ! No }
// When we are already busy getting a drink for someone // then we move to this state
def handleSpecificPerson(person: ActorRef): Receive = { // If the person asking us for a drink is the same // person we're currently handling then we'll cancel // what we were doing and service their new request case GetDrink(drinkname) if sender == person =>
pendingDelivery foreach { _.cancel() }
pendingDelivery = Some(scheduleDelivery(drinkname)) // The only time we can get the DeliverDrink message is // when we're in this state
case DeliverDrink(drink) =>
person ! drink
pendingDelivery = None // Become something new
context.become(assistInjuredPassenger orElse handleDrinkRequests)
// If we get another drink request when we're already // handling one then we punt that back to our parent // (the LeadFlightAttendant)
case m: GetDrink =>
context.parent forward m case Busy_? =>
sender ! Yes }
// Set up the initial handler
def receive = assistInjuredPassenger orElse handleDrinkRequests
}
The goal of our newFlightAttendantis to be more stateful about its current work. We use functional composition and become to change the
FlightAttendant’s behaviour, depending on whether or not it’s currently serving someone.
Figure 9.3 illustrates the transition that occurs between these states.
When it becomes the state on the right, the person with which the
FlightAttendant is currently working is bound to the receive partial function that has been instantiated (handleSpecificPerson).
The two states created are composed of theassistInjuredPassenger
partial function and either the handleDrinkRequests or the
handleSpecificPerson partial function using Scala’s orElse com- binator. Whatever message isn’t handled by assistInjuredPassengeris passed on to the next partial function. Because we intend to exercise the
assistInjuredPassengerpartial function no matter what, we compose it in both states and let the other vary.
An important thing to notice is the lack ofifstatements in this code. We would often have some sort of variable data in our classes that we would use to understand what state we’re in, but with Akka’s behavioural swapping, we don’t need to do that. For example, we don’t need code like this:
var drinkPerson: Option[ActorRef] = None
assist Injured Passenger
handle Drink Requests
handle Specific
Person Person Requesting
Drink
orElse assist
Injured Passenger
orElse
become()s when GetDrink is received Busy_?
No Yes
Busy_?
become()s when DeliverDrink is received
Figure 9.3ãHandling messages differently depending on state.
...
case GetDrink(drinkname) =>
if (drinkPerson.isDefined) // already handling someone else {
drinkPerson = Some(sender) // etc....
} ...
case DeliverDrink(drink) =>
drinkPerson foreach { _ ! drink } drinkPerson = None
That kind of coding can get oldreallyfast, so it’s pretty fantastic that we don’t need to worry about it at all. The interesting event triggers a state change and all of that state-specific code goes into that new state. It makes writing stateful code easier than chewing gum on a Wednesday afternoon.1
1Statistically speaking, this is the easiest day on which to chew gum.
On composition
Using theorElsefunction combinator to combine our behaviours is pretty standard practice, but it does come with some caveats due to the ordering of message processing.
Watch out for the eclipse
When you’re combining your receivers, you need to make sure that it’s all reachable.
def behaviourA: Receive = { case m =>
println(m) }
def behaviourB: Receive = { case MessageForB(payload) =>
doSomethingAwesomeWith payload }
// Oops!
def receive = behaviourA orElse behaviourB
Using orElse to combine your behaviours is a very powerful mecha- nism, but in the above code, we’ve got a problem. Scala is great about checking unreachable code in the following case, since it knows that the first match will always succeed, resulting in the second never being reached:
def behaviour: Receive = { case m =>
println(m)
case MessageForB(payload) =>
doSomethingAwesomeWith payload }
// error: unreachable code // case MessageForB(payload) =>
// doSomethingAwesomeWith payload
Scala cannot realize the same error in this case:
def receive = behaviourA orElse behaviourB
It will just silently let this construct, resulting inbehaviourBnever being invoked. So, when you’re combining your message handlers with orElse, ensure that you’re not eclipsing your own code.
Watch the complexity
There’s another practice that we also need to be aware of when we combine behaviours in an actor: keep it simple. It’s quite possible to lose your way and start putting too much responsibility into an actor, such that you wind up with constructs, such as:
become(protocolBehaviourStart orElse bluntHttpHandler orElse
databaseSelectionHandler orElse fileSystemLocker)
// later
become(protocolBehaviourSecondStage orElse // changed bluntHttpHandler orElse
databaseSelectionHandler orElse fileSystemLocker)
// and later still...
become(protocolBehaviourThirdStage orElse // changed activeHttpHandler orElse // changed databaseSelected orElse // changed fileSystemLocker)
// and so on...
The more behaviour you add to your actor and the more aspects you have for each behaviour, the more permutations you have and eventually it just getsoogie. The single-responsibility principle applies to actors just as much as it applies to everything else. If things get to be too much of a problem, you need to refactor your code by breaking out your responsibilities into multiple actors.
There are times, however, when the inclusion of multiple traits that carry behaviour as well as certain behavioural states in the actor itself require a requisite amount of complexity. Later, we’ll address this issue when we introduce a collection of Akka coding patterns.