michaltomanski
has many steps (e.g. user registration)
How to model a process that:
is not a trivial sequence of steps (e.g. shopping checkout)
has many steps (e.g. user registration)
How to model a process that:
Model of a (potentially complex) process in which we can distinguish steps
For people who are in a hurry
®
Is an actor
trait FSM[S, D] extends Actor with Listeners with ActorLogging
class TicketMachine(...)
extends FSM[TicketMachineState, TicketMachineData]
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
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
...
Initial values
startWith(Idle, Empty)
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
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()
}
onTransition {
case Idle -> FetchingSoonestConnections =>
nextStateData match {
case DataWithOrigin(id, origin) => {
connectionActor ! FetchSoonestConnections(origin)
}
}
...
Transition actions
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}")
}
Initializing
initialize()
Going from Idle$ to Idle$
Unhandled
whenUnhandled {
case Event(e: CreateTicketMachine, _) =>
println("Ticket machine already created!")
stay
}
Custom timers
setTimer(name, , , )
setTimer(name, , interval, )
setTimer(name, msg, interval, )
setTimer(name, msg, interval, repeat)
External monitoring
SubscribeTransitionCallBack(actorRef)
CurrentState(self, stateName)
Transition(actorRef, oldState, newState)
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]
State identifiers
trait PersistentFSM[S <: FSMState, D, E]
sealed trait TicketMachineState extends FSMState
case object Idle extends TicketMachineState {
override def identifier: String = "Idle"
}
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
Storage plugin config
akka {
persistence.journal.plugin = "akka.persistence.journal.leveldb"
persistence.journal.leveldb.dir = "leveldb/journal"
}
Storage
What is really persisted?
Recovery
What happens during recovery?
What can go wrong?
Persisting an event might fail because of:
What can go wrong?
Recovering might fail
What can go wrong?
actor ! PersistThisPlx("Important string")
actor ! PoisonPill
// might not be a good idea
What can go wrong?
Evolution
Two kinds of evolutions might cause us problems:
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)
}
}
Evolution
FSM logic evolution
Manipulating transition logic may lead to incorrect behavior during recovery. e.g.
Pros:
Cons:
Sharding FSMs
@michaltomanski
github.com/michaltomanski/fsm-demo
slides.com/michaltomanski/fsm