Express yourself with

a finite-state machine in Scala

Michał Tomański

michaltomanski

Problem(s)

  • has many steps (e.g. user registration)

How to model a process that:

Problem(s)

  • is not a trivial sequence of steps (e.g. shopping checkout)

  • has many steps (e.g. user registration)

How to model a process that:

Finite-state machine

Model of a (potentially complex) process in which we can distinguish steps

Finite-state machine

  • States
  • Data
  • Transitions
  • Triggers

Case study

Express Ticket Machine

  • Stationary (at the railway station)
  • Show the soonest train connections
  • Pick one
  • Reserve a seat on that connection
  • Pay
  • Print the ticket
  • Cancel reservation if timeout

For people who are in a hurry

®

Tools

Actor

  • Receives a message
  • Do logic related to this message
  • Update his internal state
  • Reply

FSM

Is an actor

trait FSM[S, D] extends Actor with Listeners with ActorLogging
class TicketMachine(...) 
    extends FSM[TicketMachineState, TicketMachineData]

FSM

States

class TicketMachine(...) 
    extends FSM[TicketMachineState, TicketMachineData]
sealed trait TicketMachineState

case object Idle extends TicketMachineState

case object FetchingSoonestConnections extends TicketMachineState

case object WaitingForConnectionSelection extends TicketMachineState

case object WaitingForPayment extends TicketMachineState

case object PrintingOutTickets extends TicketMachineState

FSM

Data

class TicketMachine(...) 
    extends FSM[TicketMachineState, TicketMachineData]
sealed trait TicketMachineData

case object Empty extends TicketMachineData

case class DataWithOrigin(id: Id, origin: Origin) 
  extends TicketMachineData

case class DataWithConnections(id: Id, origin: Origin, 
  connections: Seq[Connection]) extends TicketMachineData
...

FSM

Initial values

startWith(Idle, Empty)

FSM

Transitions

when(Idle) {
 case Event(CreateTicketMachine(origin), Empty) =>
   val id = TicketMachineIdGenerator.generate
   goto(FetchingSoonestConnections) using DataWithOrigin(id, origin)
when(Idle) {
 case Event(CreateTicketMachine(origin), Empty) =>
   val id = TicketMachineIdGenerator.generate
   goto(FetchingSoonestConnections) using DataWithOrigin(id, origin)
 case Event(DoSomethingElse, _) => // just for demo
   val dataUpdated = ???
   stay using dataUpdated

FSM

State timeouts

val reservationTimeout = 20.seconds
  
when(WaitingForPayment, reservationTimeout) {
  case Event(PaymentSuccessful(paymentId), data) =>
    goto(PrintingOutTickets)
  case Event(StateTimeout, data: DataWithSelectedConnection) =>
    goto(FetchingSoonestConnections) using data.resetAfterTimeout()
}

FSM

onTransition {
  case Idle -> FetchingSoonestConnections =>
    nextStateData match {
      case DataWithOrigin(id, origin) => {
        connectionActor ! FetchSoonestConnections(origin)
      }
    }
...

Transition actions

FSM

Transition actions

onTransition {
  case (a: TicketMachineState) -> (b: Idle) =>
    println("Entering Idle")
}
onTransition {
  case (a: TicketMachineState) -> (b: TicketMachineState) =>
    println(s"Going from ${a.getClass.getSimpleName} 
      to ${b.getClass.getSimpleName}")
}

FSM

Initializing

initialize()

Going from Idle$ to Idle$

FSM

Unhandled

whenUnhandled {
  case Event(e: CreateTicketMachine, _) =>
    println("Ticket machine already created!")
    stay
}

Demo time

How was it?

  • Process manager that behaves like an actor
  • Looks like a user story
  • Easy to maintain and extend

Not presented, but also cool

FSM

Custom timers

setTimer(name,    ,         ,       )
setTimer(name,    , interval,       )
setTimer(name, msg, interval,       )
setTimer(name, msg, interval, repeat)

FSM

External monitoring

SubscribeTransitionCallBack(actorRef)
CurrentState(self, stateName)
Transition(actorRef, oldState, newState)

Awesome, but can it be any better?

Next Level - make it persistent

Persistent Actor

  • Persist every valid event
  • Do your business logic after it's persisted
  • When it crashes, all the events are read from the journal
  • We got our state back!

Persistent FSM

Is a persistent actor

trait PersistentFSM[S <: FSMState, D, E] 
  extends PersistentActor with PersistentFSMBase[S, D, E]
  with ActorLogging
class TicketMachine(...) 
  extends PersistentFSM[TicketMachineState, 
    TicketMachineData, TicketMachineEvent]

Persistent FSM

State identifiers

trait PersistentFSM[S <: FSMState, D, E] 
sealed trait TicketMachineState extends FSMState

case object Idle extends TicketMachineState {
  override def identifier: String = "Idle"
}

Persistent FSM

Applying events

goto(FetchingSoonestConnections) applying TicketMachineCreated(id, origin)
override def applyEvent(domainEvent: TicketMachineEvent,
 currentData: TicketMachineData) =
  domainEvent match {
    case TicketMachineCreated(id, origin) =>
      DataWithOrigin(id, origin)
    ...
goto(FetchingSoonestConnections) using DataWithOrigin(id, origin)

FSM

PersistentFSM

Persistent FSM

Storage plugin config

akka {
  persistence.journal.plugin = "akka.persistence.journal.leveldb"
  
  persistence.journal.leveldb.dir = "leveldb/journal"
}

Demo time

Persistent FSM

Storage

What is really persisted?

  • Every event that is applied
  • The state identifier of the states we are going to 

Persistent FSM

Recovery

What happens during recovery?

  • events are read from journal one by one
  • applyEvent is run on each domain event
  • state is set to the state corresponding to its identifier
  • after the entire journal is read, the transition handlers from last state to last state is performed
  • Going from WaitingForPayment to WaitingForPayment
  • onRecoveryCompleted handler is fired

Persistent FSM

What can go wrong?

Persisting an event might fail because of:

  • Pre-persisting failure (e.g. serialization error)
    - ​onPersistRejected is called
     
  • Persisting failure (e.g. db not available)
    - onPersistFailure is called and actor is stopped
    - backoff strategy can be configured
    - journal plugin decides when to give up and issue error

Persistent FSM

What can go wrong?

Recovering might fail

  • onRecoveryFailure will be called
    and actor will be stopped

Persistent FSM

What can go wrong?

  • Persisting events can take some time
  • All incoming events are stashed 
  • Posion pill is handled by akka itself, not minding events waiting in the stash
actor ! PersistThisPlx("Important string")
actor ! PoisonPill
// might not be a good idea

Persistent FSM

What can go wrong?

  • Events during recovery have /deadLetters as sender() 
  • if you really need to have the original sender, store its actorpath

Persistent FSM

Evolution

Two kinds of evolutions might cause us problems:

  • Events schema evolution
  • FSM logic evolution

Persistent FSM

Evolution

Events schema evolution

class MyEventAdapter extends EventAdapter {
  override def manifest(event: Any): String =
    "" // when no manifest needed, return ""
 
  override def toJournal(event: Any): Any =
    event // identity
 
  override def fromJournal(event: Any, manifest: String): EventSeq = {
    val upcastedEvent = doSomeLogic(event)
    EventSeq.single(upcastedEvent)
  }
}

Persistent FSM

Evolution

FSM logic evolution

Manipulating transition logic may lead to incorrect behavior during recovery. e.g.

  • run our state machine through steps 1, 2 and 3
  • shut it down
  • remove transition between state 2 and 3
  • restart everything
  • ...aaand we have all events correctly replayed and ended up in state 3, although it should be now impossible

Ok, is it better now with Persistent FSM?

Pros:

  • Persistence
  • A good start for Event Sourcing / CQRS

 

Cons:

  • Schema changes (not so painful)
  • Logic changes (a bit more painful)

Persistent FSM

Awesome, but...

Sharding FSMs

Express yourself with

a finite-state machine in Scala

Michał Tomański

@michaltomanski

github.com/michaltomanski/fsm-demo

slides.com/michaltomanski/fsm

Made with Slides.com