Akka FSM

The Manager pushes the cookie-making process forward by coordinating all other inhabitants of the bakery. It does so by taking the messages representing job results from one actor and passing them further along to an appropriate successor. It is important that this process is consequent, that is, it should be impossible to make raw cookies at the moment since we only have a shopping list and no dough. We'll represent this behavior as a state machine. 

An FSM is an abstraction defined by a set of states the machine can be in. For each state, it also defines which message types can be accepted in this state and the possible reaction of the machine, including its new state.

Let's dive into the code and see how this approach is implemented with Akka FSM.

The actor is defined by extending the FSM trait:

class Manager(chef: ActorRef, cook: ActorRef, baker: ActorRef) extends FSM[State, Data]

The type parameter, State, represents the type of states the actor can be in, and the Data represents the possible associated internal state.

There is an obvious confusion between the term State, referring to the state of the FSM our actor represents, and a State referring to the data which is associated with each of the steps in the process. To avoid ambiguity, we'll further refer to the state of the actor as Data and the state of the FSM as State.

States of the actor reflect the processes that occur in the bakery: the goods moving from the shop boy over to the chef and cook, which then move over to the baker and back to the manager (note that because of the sequential art of work-passing done by the manager, there will be only one worker actor active in our bakery at the moment, even if they could work in parallel with a more sophisticated manager).

The following messages represent the state of the managed bakery:

trait State
case object Idle extends State
case object Shopping extends State
case object Mixing extends State
case object Forming extends State
case object Baking extends State

The messages we defined previously also need to be extended to represent the possible types of data:

sealed trait Data
case object Uninitialized extends Data
final case class ShoppingList(...) extends Data
final case class Groceries(...) extends Data
final case class Dough(weight: Int) extends Data
final case class RawCookies(count: Int) extends Data

The FSM itself is defined by describing three primary aspects of the state machine:

  • States
  • State transitions
  • An initial state

Let's take a look at the actor's code to see how this is done. The states are defined within the when block, which accepts a state name and a state function:

when(Idle) {
case Event(s: ShoppingList, Uninitialized) ⇒
goto(Shopping) forMax (5 seconds) using s
case _ =>
stay replying "Get back to work!"
}

When there are multiple when blocks for the same states, the state functions that constitute them are concatenated.

The state function is a PartialFunction[Event, State], and describes a new state for each event type received in a particular state. Akka FSM provides a nice domain specific language (DLS) for this. For example, in the preceding code, the actor reacts to the ShoppingList event by transitioning to the Shopping state with a timeout of 5 seconds. The shopping list is used as new state data.

In the case of any other message, the actor stays in the same state and replies to the sender with a friendly remark.

In the Shopping state, the Manager reacts differently depending upon whether the groceries conform to the shopping list or not:

when(Shopping) {
case Event(g: Groceries, s: ShoppingList)
if g.productIterator sameElements s.productIterator ⇒
goto(Mixing) using g
case Event(_: Groceries, _: ShoppingList) ⇒
goto(Idle) using Uninitialized
}

In the first case, it uses Groceries as a new state and goes to the next state. In the second case, it goes back to the Idle state and sets its state to Uninitialized.

Other states are described in a similar fashion:

when(Mixing) {
case Event(p: Dough, _) ⇒
goto(Forming) using p
}

when(Forming) {
case Event(c: RawCookies, _) ⇒
goto(Baking) using c
}

when(Baking, stateTimeout = idleTimeout * 20) {
case Event(c: Cookies, _) ⇒
log.info("Cookies are ready: {}", c)
stay() replying "Thank you!"
case Event(StateTimeout, _) =>
goto(Idle) using Uninitialized
}

We're just moving on to the next state and updating the state data in the process. The most obvious observation at the moment would be that This actor does nothing but enjoying himself, and we're going to fix this by using the onTransition block, which describes the behavior of the actor at the moment state transition occurs:

onTransition {
case Idle -> Shopping ⇒
val boy = sendBoy
boy ! stateData
case Shopping -> Idle =>
self ! stateData
case Shopping -> Mixing ⇒
chef ! nextStateData
case Mixing -> Forming =>
cook ! nextStateData
case Forming -> Baking =>
baker ! nextStateData
}

The Manager already knows its subordinates, so it only needs to look up a Boy. Then, for each of the state transitions, it obtains the necessary state by using either stateData or nextStateData, which references the actor's state data before and after the respective state transition. This data is sent to the appropriate subordinate.

Now, all that's missing is an optional whenUnhandled block, which is executed in all states. The timer setting and a mandatory initiate() call sets up the defined timer and performs a state transition to the initial state.

Akka FSM forces you to mix business logic with actor-related code, which makes it hard to test and support it. It also locks you into the provided implementation and makes it impossible to bring in another state machine realization. Always consider another possibility before fixing upon Akka FSM.

At the same time, by separating the definition of states separately from behavior, Akka FSM allows for clean structuring of the business logic.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.81.33