Finite State Machine (FSM) with Akka Actor
A finite state machine (sometimes called a finite state automaton) is a computation model that can be implemented with hardware or software and can be used to simulate sequential logic and some computer programs, which brings a lot of possibilities to think the state and action in terms of natural language.
When we say FSM , two mandatory attributes are attached with this terminology to achieve any computational goal are State and Data.
State and Data are often used for transition from one state to another with the help of Event capture. Event is the wrapper that always expects State and Data to capture and perform the action to change the state along with the data.
Akka has FSM Actor ( is eventually a trait in scala) which expects two parameters State and Data.
Let’s try to understand how FSM can be implemented using Akka Actor in Scala.
State and Data for FSM actor
class VendingMachineFSM extends FSM[VendingState, VendingData] { }
The above snippet means the Vending FSM from the VendingState
and the data that is shared between these various States is just VendingData.
Now Since we have the VendingState
trait and VendingData
, Let’s look into their structure
trait VendingState
case object Idle extends VendingState
case object Operational extends VendingState
case object WaitForMoney extends VendingState
Similarly for VendingData
trait VendingData
case object Uninitialized extends VendingDatacase class Initialized(inventory: Map[String, Int], prices: Map[String, Int]) extends VendingDatacase class WaitForMoneyData(inventory: Map[String, Int], prices: Map[String, Int], product: String, money: Int, requester: ActorRef) extends VendingData
So, as we see in the above structure, we have three States — Idle
, Operational
and WaitForMoney
. Our data, the VendingData
holds Uninitialized
in the beginning, so when the machine starts from dispensing the order it will the preliminary data as Uninitialized
along with state Idle
which we will see in the example shortly, right after that we can send the event Event(Initialize(inventory, prices), Uninitialized)
in the idle state block, after that, it will go to Operational
where event Event(RequestProduct(product), Initialized(inventory, prices))
gets capture and then WaitForMoney
state.
Of course, after all these, we will have three more important partial function for the entire state transition and operations whenUnhandled
,onTransition
,initialize()
Now let’s see how to capture each state in when
partial function.
class VendingMachineFSM extends FSM[VendingState, VendingData] {
startWith(Idle, Uninitialized)
when(Idle) { ... ...
}
when(Operational) {
... ...
}
when(WaitForMoney, stateTimeout = 1 second) {
... ... }
whenUnhandled {
... ...
stay()
}
onTransition {
case stateA -> stateB =>
//FSM
log.info(s"Transitioning from $stateA to $stateB")
}
initialize()
}
We have an initial State (which is Idle
) and any messages that are being sent to the Machine during Idle
The state is handled in the when(Idle)
block, Operational
the state is handled in when(Operational)
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 the 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 the case Event(Initialize(inventory, prices), Uninitialized),
also at any point of time when event is not satisfied and we want the user to re-attempt the action, we can use stay()
method and wait to make a correction to take it forward from operational
state.
For event from 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
We also notice that the when
the function accepts two mandatory parameters - the first being the name of the State itself, eg. Idle
, Operational
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 the next post). So, the code block would look something like
when(Idle) {
case Event(Initialize(inventory, prices), Uninitialized) =>
goto(Operational) using Initialized(inventory, prices)
// equivalent with context.become(operational(inventory, prices))
case _ =>
sender() ! VendingError("MachineNotInitialized")
stay()
}
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
.
Finally, there is an onTransition
function which allows you to react or get notified of changes in States, mostly this is used for logging and even we can create another FSM based on individual state capture.
Full code we can get it here, Happy Learning :)