Akka Notes – Finite State Machines – 1
I recently had the opportunity to play with Akka FSM at work for some really interesting use-case. The API (in fact, the DSL) is pretty awesome and the entire experience was amazing. Here’s my attempt to log my notes on building a Finite State Machine using Akka FSM. As an example, we’ll walk through the steps of building an (limited) Coffee vending machine.
Why not become
and unbecome
?
We know that the plain vanilla Akka Actors can switch its behavior by using become/unbecome. Then, why do we need Akka FSM? Can’t a plain Actor just switch between the States and behave differently? Yes, it could. But while Akka’s become and unbecome is most often enough to switch the behavior of Actors with a couple of states involved, building a State Machine with more than a few states quickly makes the code hard to reason with (and even harder to debug).
Not surprisingly, the popular recommendation is to switch to Akka FSM if there are more than 2 states in our Actor.
What’s Akka FSM
To expand on it further, Akka FSM is Akka’s approach to building Finite State Machines that simplifies management of behavior of an Actor in various states and transitions between those states.
Under the hood, Akka FSM is just a trait that extends Actor.
trait FSM[S, D] extends Actor with Listeners with ActorLogging
What this FSM
trait provides is pure magic – it provides a DSL that wraps a regular Actor enabling us to focus on building the state machine that we have in hand, faster.
In other words, our regular Actor has just one receive
function and the FSM trait wraps a sophisticated implementation of the receive
method which delegates calls to the block of code that handles the data in a particular state.
One other good thing I personally noticed is that after writing, the complete FSM Actor still looks clean and easily readable.
Alright, let’s get to the code. Like I said, we will be building a Coffee Vending Machine using Akka FSM. The State Machine looks like this :
State and Data
With any FSM, there are two things involved at any moment in a FSM – the State
of the machine at any point in time and the Data
that is shared among the states. In Akka FSM, in order to check which is our Data and which are the States, all we need to do is to check its declaration.
class CoffeeMachine extends FSM[MachineState, MachineData]
This simply means that all the States of the FSM extend from the MachineState
and the data that is shared between these various States is just MachineData
.
As a matter of style, just like with normal Actor where we declare all our messages in a companion object, we declare our States and Data in a companion object:
object CoffeeMachine { sealed trait MachineState case object Open extends MachineState case object ReadyToBuy extends MachineState case object PoweredOff extends MachineState case class MachineData(currentTxTotal: Int, costOfCoffee: Int, coffeesLeft: Int) }
So, as we captured in the State Machine diagram, we have three States – Open
, ReadyToBuy
and PoweredOff
. Our data, the MachineData
holds (in reverse order) the numbers of coffees that the vending machine could dispense before it shuts itself down (coffeesLeft
), the price of each cup of coffee (costOfCoffee
) and finally, the amount deposited by the vending machine user (currentTxTotal
) – if it is less than the cost of the coffee, the machine doesn’t dispense coffee, if it is more, then we ought to give back the balance cash.
That’s it. We are done with the States and the Data.
Before we go through the implementation of each of the States that the vending machine can be in and the various interactions that the user can have with the machine at a particular State, we’ll have a 50,000 feet view of the FSM Actor itself.
Structure of FSM Actor
The structure of the FSM Actor looks very similar to the State Machine diagram itself and it looks like this :
class CoffeeMachine extends FSM[MachineState, MachineData] { //What State and Data must this FSM start with (duh!) startWith(Open, MachineData(..)) //Handlers of State when(Open) { ... ... when(ReadyToBuy) { ... ... when(PoweredOff) { ... ... //fallback handler when an Event is unhandled by none of the States. whenUnhandled { ... ... //Do we need to do something when there is a State change? onTransition { case Open -> ReadyToBuy => ... ... ... }
What we understand from the structure:
1) We have an initial State (which is Open
) and any messages that is being sent to the Machine during Open
State is handled in the when(Open)
block, ReadyToBuy
state is handled in when(ReadyToBuy)
block and so on. The messages that I am referring to here are just like regular messages that we tell
a plain Actor except that in case of FSMs, the message is wrapped along with the Data as well. The wrapper is called an Event
(akka.actor.FSM.Event
) and an example would look like Event(deposit: Deposit, MachineData(currentTxTotal, costOfCoffee, coffeesLeft))
From the Akka documentation :
/** * All messages sent to the [[akka.actor.FSM]] will be wrapped inside an * `Event`, which allows pattern matching to extract both state and data. */ case class Event[D](event: Any, stateData: D) extends NoSerializationVerificationNeeded
2) We also notice that the when
function accepts two mandatory parameters – the first being being the name of the State itself, eg. Open
, ReadyToBuy
etc and the second argument is a PartialFunction, just like an Actor’s receive
where we do pattern matching. The most important thing to note here is that each of these pattern matching case
blocks must return a State (more on this in next post). So, the code block would look something like
when(Open) { case Event(deposit: Deposit, MachineData(currentTxTotal, costOfCoffee, coffeesLeft)) => { ... ...
3) Generally, only those messages that match the patterns declared inside the when
‘s second argument gets handled at a particular State. If there is no matching pattern, then the FSM Actor tries to match our message to a pattern declared in the whenUnhandled
block. Ideally, all the messages that are common across all the States is coded away in the whenUnhandled
. (I am not the one to suggest style but alternatively, you could declare smaller PartialFunctions and compose them using andThen
if you would like to reuse pattern matching across selected States)
4) Finally, there is an onTransition
function which allows you to react or get notified of changes in States.
Interactions/Messages
There are two kinds of people who interact with this Vending machine – Coffee drinkers, who need coffee and Vendors, who do the machine’s administrative tasks.
For sake of organization, I have introduced two traits for all the interactions with the Machine. (Just to refresh, an Interaction/Message is the first element wrapped inside the Event along with the MachineData). In plain old Actor terms, this is equivalent to the message that we send to the Actors.
object CoffeeProtocol { trait UserInteraction trait VendorInteraction ... ...
VendorInteraction
Let’s also declare the various interactions that a Vendor can make with the machine.
case object ShutDownMachine extends VendorInteraction case object StartUpMachine extends VendorInteraction case class SetCostOfCoffee(price: Int) extends VendorInteraction //Sets Maximum number of coffees that the vending machine could dispense case class SetNumberOfCoffee(quantity: Int) extends VendorInteraction case object GetNumberOfCoffee extends VendorInteraction
So, the Vendor can
- start and shutdown the machine
- set the price of the coffee and
- set and get the number of coffee remaining in the machine.
UserInteraction
case class Deposit(value: Int) extends UserInteraction case class Balance(value: Int) extends UserInteraction case object Cancel extends UserInteraction case object BrewCoffee extends UserInteraction case object GetCostOfCoffee extends UserInteraction
Now, for the UserInteraction, the User can
- deposit money to buy a coffee
- get dispensed the extra cash if the deposited money is more than the cost of the coffee
- ask the machine to brew coffee if the deposit money is equal or more than the cost of the coffee
- cancel the transaction before brewing the coffee and get back all the money deposited
- query the machine for the cost of the coffee.
In the next post, we’ll go through each of the States and explore on their interactions (along with testcases) in detail.
Code
For the benefit of the impatient, the entire code is available on github.
Reference: | Akka Notes – Finite State Machines – 1 from our JCG partner Arun Manivannan at the Rerun.me blog. |