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
BeeScala FSM
By Michał Tomański
BeeScala FSM
Presentation on FSM in Scala made for BeeScala conference in Slovenia, 2016
- 2,052