The stateful flight attendant

Một phần của tài liệu Artima akka concurrency (Trang 230 - 237)

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.

Một phần của tài liệu Artima akka concurrency (Trang 230 - 237)

Tải bản đầy đủ (PDF)

(515 trang)