What if the flight attendants had specific areas of the plane in which they were supposed to work? In other words, the flight attendant in charge of rows one to ten isn’t supposed to handle rows twenty one to thirty.
To handle this type of routing, we can create a custom router in code. The router will statically allocate several FlightAttendants and then route based on the row number contained in the incoming sender’s
sender.path.nameattribute.
package zzz.akka.avionics
import akka.actor.{Props, SupervisorStrategy}
import akka.routing.{RouterConfig, RouteeProvider, Route, Destination}
import akka.dispatch.Dispatchers
class SectionSpecificAttendantRouter extends RouterConfig { this: FlightAttendantProvider =>
// The RouterConfig requires us to fill out these two // fields. We know what the supervisorStrategy is but we're // only slightly aware of the Dispatcher, which will be // discussed in detail later
def routerDispatcher: String = Dispatchers.DefaultDispatcherId def supervisorStrategy: SupervisorStrategy =
SupervisorStrategy.defaultStrategy
// The createRoute method is what invokes the decision // making code. We instantiate the Actors we need and then // create the routing code
def createRoute(routeeProvider: RouteeProvider): Route = { // Create 5 flight attendants
val attendants = (1 to 5) map { n =>
routeeProvider.context.actorOf(Props(newFlightAttendant),
"Attendant-" + n) }
// Register them with the provider - This is important.
// If you forget to do this, nobody's really going to // tell you about it :)
routeeProvider.registerRoutees(attendants)
// Now the partial function that calculates the route.
// We are going to route based on the name of the // incoming sender. Of course, you would cache this or // do something slicker.
{
case (sender, message) =>
import Passenger.SeatAssignment
val SeatAssignment(_, row, _) = sender.path.name List(Destination(sender,
attendants(math.floor(row.toInt / 11).toInt))) }
} }
Above is the definition of our new
SectionSpecificAttendantRouter. You should ignore all of the hard-coded numbers if they make you squeamish—neither of us would do such horrific things in the real world.
The goal of our new router is to produce an instance ofRoute, which is defined as:
PartialFunction[(ActorRef, Any), Iterable[Destination]]
And the destination is defined as:
case class Destination(sender: ActorRef, recipient: ActorRef)
You might note from the code, and from the definitions above, that the
createRoutemethod can actually return a host of sender/recipient pairs in the Iterableof destinations, so while the world is essentially your oyster here, we keep it simple.
Testing theSectionSpecificAttendantRouter
There’s a lot of “stuff” to a router, but the main bit that’s important is the actual routing aspect of it, and that’s what needs to be tested. The folks that coded Akka are, as we’ve already seen, concerned about testing and have provided the ability to extract theRouteobject from the router, which makes it pretty easy to test.
The team listens
When I wrote this chapter, I sent an email to the team asking for some help on testing a custom router. At the time, there was no way to get inside the router’s logic to test the decision making process. The test structure I had to set up was really quite convoluted—it was more complicated than the code I was trying to test!
In response to my question, the team made the addition of the ExtractRouteobject, and that simplified thingsgreatly. This is just one of many examples of a team that listens and reacts to the commu- nity that uses its product. They’re a fantastic group ofhakkers.
package zzz.akka.avionics
import akka.actor.{Props, ActorSystem, Actor, ActorRef}
import akka.testkit.{TestKit, ImplicitSender, ExtractRoute}
import akka.routing.{RouterConfig, Destination}
import org.scalatest.{WordSpec, BeforeAndAfterAll}
import org.scalatest.matchers.MustMatchers
// This will be the Routee, which will be put in place // instead of the FlightAttendant
class TestRoutee extends Actor { def receive = Actor.emptyBehavior }
// We need a passenger as well. It's the same implementation // as the TestRoutee but it's nice to have names for things class TestPassenger extends Actor {
def receive = Actor.emptyBehavior }
class SectionSpecificAttendantRouterSpec
extends TestKit(ActorSystem("SectionSpecificRouterSpec")) with ImplicitSender
with WordSpec
with BeforeAndAfterAll with MustMatchers { override def afterAll() {
system.shutdown() }
// A simple method to create a new
// SectionSpecificAttendantRouter with the overridden // FlightAttendantProvider that instantiates a TestRoutee def newRouter(): RouterConfig =
new SectionSpecificAttendantRouter with FlightAttendantProvider {
override def newFlightAttendant() = new TestRoutee }
def passengerWithRow(row: Int) =
system.actorOf(Props[TestPassenger], s"Someone-$row-C") val passengers = (1 to 25).map(passengerWithRow)
"SectionSpecificAttendantRouter" should {
"route consistently" in { val router =
system.actorOf(Props[TestRoutee]
.withRouter(newRouter()))
// Extract the Route object from the router val route = ExtractRoute(router)
// Route for the first 10 passengers val routeA: Iterable[Destination] =
passengers.slice(0, 10).flatMap { p =>
route(p, "Hi") }
// Make sure they all went to the same place routeA.tail.forall { dest =>
dest.recipient == routeA.head.recipient } must be (true)
// Route across the different sections val routeAB: Iterable[Destination] =
passengers.slice(9, 11).flatMap { p =>
route(p, "Hi") }
// Ensure that they did cross sections routeAB.head must not be (routeAB.tail.head)
// Route for the next 10 passengers val routeB: Iterable[Destination] =
passengers.slice(10, 20).flatMap { p =>
route(p, "Hi") }
// Make sure they all went to the same place routeB.tail.forall { dest =>
dest.recipient == routeB.head.recipient } must be (true)
} } }
That’s it. Being able to get at theRouteobject directly lets us poke and prod it to see what it does. Simple and elegant.