Behaviors as Finite state machines
With untyped actors there is explicit support for building Finite State Machines. No support is needed in Akka Typed as it is straightforward to represent FSMs with behaviors.
To see how the Akka Typed API can be used to model FSMs here’s the Buncher example ported from the untyped actor FSM docs. It demonstrates how to:
- Model states using different behaviors
- Model storing data at each state by representing the behavior as a method
- Implement state timeouts
The events the FSM can receive become the type of message the Actor can receive:
- Scala
-
source
// FSM event becomes the type of the message Actor supports sealed trait Event final case class SetTarget(ref: ActorRef[Batch]) extends Event final case class Queue(obj: Any) extends Event case object Flush extends Event case object Timeout extends Event
- Java
-
source
interface Event {} static final class SetTarget implements Event { private final ActorRef<Batch> ref; public SetTarget(ActorRef<Batch> ref) { this.ref = ref; } public ActorRef<Batch> getRef() { return ref; } } final class Timeout implements Event {} static final Timeout TIMEOUT = new Timeout(); public enum Flush implements Event { FLUSH } static final class Queue implements Event { private final Object obj; public Queue(Object obj) { this.obj = obj; } public Object getObj() { return obj; } }
SetTarget
is needed for starting it up, setting the destination for the Batches
to be passed on; Queue
will add to the internal queue while Flush
will mark the end of a burst.
Untyped FSM
s also have a D
(data) type parameter. Akka Typed doesn’t need to be aware of this and it can be stored via defining your behaviors as methods.
- Scala
-
source
sealed trait Data case object Uninitialized extends Data final case class Todo(target: ActorRef[Batch], queue: immutable.Seq[Any]) extends Data
- Java
-
source
interface Data {} final class Todo implements Data { private final ActorRef<Batch> target; private final List<Object> queue; public Todo(ActorRef<Batch> target, List<Object> queue) { this.target = target; this.queue = queue; } public ActorRef<Batch> getTarget() { return target; } public List<Object> getQueue() { return queue; } @Override public String toString() { return "Todo{" + "target=" + target + ", queue=" + queue + '}'; } public Todo addElement(Object element) { List<Object> nQueue = new LinkedList<>(queue); nQueue.add(element); return new Todo(this.target, nQueue); } public Todo copy(List<Object> queue) { return new Todo(this.target, queue); } public Todo copy(ActorRef<Batch> target) { return new Todo(target, this.queue); } }
Each state becomes a distinct behavior. No explicit goto
is required as Akka Typed already requires you return the next behavior.
- Scala
-
source
// states of the FSM represented as behaviors def idle(data: Data): Behavior[Event] = Behaviors.receiveMessage[Event] { message: Event => (message, data) match { case (SetTarget(ref), Uninitialized) => idle(Todo(ref, Vector.empty)) case (Queue(obj), t @ Todo(_, v)) => active(t.copy(queue = v :+ obj)) case _ => Behaviors.unhandled } } def active(data: Todo): Behavior[Event] = Behaviors.withTimers[Event] { timers => // instead of FSM state timeout timers.startSingleTimer(Timeout, Timeout, 1.second) Behaviors.receiveMessagePartial { case Flush | Timeout => data.target ! Batch(data.queue) idle(data.copy(queue = Vector.empty)) case Queue(obj) => active(data.copy(queue = data.queue :+ obj)) } }
- Java
-
source
// FSM states represented as behaviors private static Behavior<Event> uninitialized() { return Behaviors.receive(Event.class) .onMessage( SetTarget.class, (context, message) -> idle(new Todo(message.getRef(), Collections.emptyList()))) .build(); } private static Behavior<Event> idle(Todo data) { return Behaviors.receive(Event.class) .onMessage(Queue.class, (context, message) -> active(data.addElement(message))) .build(); } private static Behavior<Event> active(Todo data) { return Behaviors.withTimers( timers -> { // State timeouts done with withTimers timers.startSingleTimer("Timeout", TIMEOUT, Duration.ofSeconds(1)); return Behaviors.receive(Event.class) .onMessage(Queue.class, (context, message) -> active(data.addElement(message))) .onMessage( Flush.class, (context, message) -> { data.getTarget().tell(new Batch(data.queue)); return idle(data.copy(new ArrayList<>())); }) .onMessage( Timeout.class, (context, message) -> { data.getTarget().tell(new Batch(data.queue)); return idle(data.copy(new ArrayList<>())); }) .build(); }); }
To set state timeouts use Behaviors.withTimers
along with a startSingleTimer
.
Any side effects that were previously done in a onTransition
block go directly into the behaviors.