FSM Actors in Akka

by
Tags: , ,
Category:

Akka provides a nice framework for implementing a finite state machine with it’s FSM trait. In this post we’ll take a look at some of the features that FSM provides for an actor. The main documentation for this feature is in the Akka documentation, while the API is in the scaladoc. The sample code for this post can be found at scala-samples/akka-fsm.

State and Data

As with any finite state machine, an Akka FSM actor is modeled around the concept of state. The actor must be in one of the defined states at all times. States in FSM are modeled in the type system. Typically an FSM implementation will define a common trait and have a case object for each state that extends the common trait. If we’re building a tic-tac-toe game, we might define the states below.

trait GameState
case object PlayerXTurn extends GameState
case object PlayerOTurn extends GameState
case object GameOver extends GameState

In addition to state, Akka FSM allows us to attach a data object to each state. As with state, it is common to define a trait and then have case objects or case classes extending that common trait. We could have different types for each state in our FSM, more data types than states, or whatever makes sense for the actor. In this case, we’ll just define our data type as a Map[Int, Option[Player]]. The keys of this map will be numbers representing the squares (numbered 0-8, left to right), while the values indicate which player is occupying the square. The value will be None if there is no player in the square, or Some(X) or Some(O)f when one of the players has occupied the square.

Once types are defined for the state and data, we can mix in FSM to the Actor. FSM must be parameterized with the types we’ll be using for state and data.

trait Player
class Game extends Actor with FSM[GameState, Map[Int, Option[Player]]]

The last thing to do is to define what state the FSM should start at and the data at that state. That is defined with a call to the startWith method in the actor body. In this case, we’ll start at PlayerXTurn with an empty game board.

class Game extends Actor with FSM[GameState, Map[Int, Option[Player]]] {
  startWith(PlayerXTurn, Map.empty ++ (0 to 8 map(i => i -> None)))
}

Reacting to Events

Once the an actor starts, it’s probably going to be getting some messages. In a plain Akka actor, any object can be sent to the actor as a message. An FSM actor is no different, but the messages are wrapped in an instance of Event, which also provides the current state data. The way that we react to a message will likely be different depending on what state the FSM is currently in. The when method is used to define behavior for a specific state of the actor. The code below show the start of defining behavior to be applied when the FSM state is PlayerXTurn.

class Game extends Actor with FSM[GameState, Map[Int, Option[Player]]] {
  ...
  when(PlayerXTurn) {
    // TODO: define behavior
  }
}

The second argument to when is a PartialFunction[Event, GameState], which effectively means that we need to define a function that accepts Event as an input and returns GameState. Partial functions are typically implemented with a pattern match as shown next.

class Game extends Actor with FSM[GameState, Map[Int, Option[Player]]] {
  ...
  when(PlayerXTurn) {
    case Event(PlayerXMove(squareId), gameBoard) if gameBoard(squareId) == None =>
      // TODO: handle valid move
    case _ =>
      // TODO: handle invalid move
  }
}

The curly braces now contain a pattern match on Event. The first case matches when an Event is received where the message is PlayerXMove. We bind the requested square to squareId and the current state to gameBoard. The pattern is further restricted to only match if the requested square contains None, meaning that it is not occupied. The second case is a wildcard match, so it will match on anything not matched by the first case. This case handles the case that we get a PlayerOMove message or any other message.

If we have a valid PlayerXMove, then the state machine should transition to PlayerOTurn and the game data at that state should be updated to inlude X’s move. FSM provides the goto method, which takes a state as it’s argument. Often goto is followed by a call to the using method, to which we pass the data for the state.

class Game extends Actor with FSM[GameState, Map[Int, Option[Player]]] {
  ...
  when(PlayerXTurn) {
    case Event(PlayerXMove(squareId), gameBoard) if gameBoard(squareId) == None =>
      val newBoard = gameBoard + (squareId -> Some(X))
      goto(PlayerOTurn) using(newBoard) replying(newBoard)
    case _ =>
      // TODO: handle invalid move
  }
}

The call to replying on line 6 allows us to send a message back to the sender of the PlayerXTurn message. Here we are sending the updated game board back to the sender.

When the wildcard case matches, an invalid message has been sent to the actor. We don’t want to change state or update the game data, but we will send back InvalidMove to the sender.

class Game extends Actor with FSM[GameState, Map[Int, Option[Player]]] {
  ...
  when(PlayerXTurn) {
    case Event(PlayerXMove(squareId), gameBoard) if gameBoard(squareId) == None =>
      val newBoard = gameBoard + (squareId -> Some(X))
      goto(PlayerOTurn) using(newBoard) replying(newBoard)
    case _ =>
      stay() replying(InvalidMove)
  }
}

The stay method is invoked to say that the Event has been handled but we don’t want to transition to a new state.

The game actor will have another when call that defines the behavior for PlayerOTurn, which will be very similar to the behavior for PlayerXTurn.

Finally, we’ll have a when call that defines the behavior in GameOver state to simply return the final game board to the caller.

Testing an FSM Actor

Of course, we must be able to write tests for our FSM actor. Akka provides nice testing support via it’s TestKit infrastructure, which includes some FSM-specific support as well. The first step in testing an FSM actor is to create an instance of it with a call to TestFSMRef, which will return an ActorRef. This actor, however, is a synchronous actor that’s specifically designed for unit testing. Unlike a normal actor, it behaves synchronously and exposes internal state. The stateName and stateData properties let us verify that the FSM actor is behaving the way we expect.

Any messages sent to the actor are processed synchronously. When we send a message with ask, the message is processed and the Future returned is already completed. Again, this is all to make unit testing easy and is not the normal behavior of an actor.

class GameSpec extends TestKit(ActorSystem("game-spec-system")) with FlatSpecLike with Matchers with ScalaFutures {
  val EmptyBoard = Map.empty[Int, Option[Player]] ++ (0 to 8).map(i => i -> None)
  "GameActor" should "allow X player to move first in new game" in {
    val fsm = TestFSMRef(new Game)
    fsm.stateName shouldBe PlayerXTurn
    fsm.stateData shouldBe EmptyBoard
    fsm ! PlayerXMove(4)
    fsm.stateData shouldBe EmptyBoard + (4 -> Some(X))
  }
}

The test above begins by creating an instance of the FSM actor class Game and assigning the ActorRef to fsm. It then verifies that a newly created game starts in the state PlayerXTurn and that the data in this state is an empty board (one where no square is occupied). It then sends a PlayerXMove message to the actor saying that X would like to occupy the center square, and then checks that the game board is updated to show Some(X) in square 4.

Wrapping Up

We’ve walked through a quick introduction to using the FSM trait in Akka. I think this trait provides a nice framework for implementing an actor in terms of states and transitions. This example is quite simple, and so it may seem like overkill in this case. I think the value of the FSM trait lies in making the concepts of state and state data explicit. The ability to test an FSM actor’s current state and data is also a nice feature, which I think may be harder to do with alternate patterns.

A more complex FSM actor can be seen in the reactive-quizzo sample code in Game.scala