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
// 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
interface Event { }
public 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 {}
final static Timeout TIMEOUT = new Timeout();
public enum Flush implements Event {
    FLUSH
}
public 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 FSMs 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
sealed trait Data
case object Uninitialized extends Data
final case class Todo(target: ActorRef[Batch], queue: immutable.Seq[Any]) extends Data
Java
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
// states of the FSM represented as behaviors
def idle(data: Data): Behavior[Event] = Behaviors.receiveMessage[Event] { msg: Event ⇒
  (msg, 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
// FSM states represented as behaviors
private static Behavior<Event> uninitialized() {
    return Behaviors.receive(Event.class)
            .onMessage(SetTarget.class, (ctx, msg) -> idle(new Todo(msg.getRef(), Collections.emptyList())))
            .build();
}

private static Behavior<Event> idle(Todo data) {
    return Behaviors.receive(Event.class)
            .onMessage(Queue.class, (ctx, msg) -> active(data.addElement(msg)))
            .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, (ctx, msg) -> active(data.addElement(msg)))
                .onMessage(Flush.class, (ctx, msg) -> {
                    data.getTarget().tell(new Batch(data.queue));
                   return idle(data.copy(new ArrayList<>()));
                })
                .onMessage(Timeout.class, (ctx, msg) -> {
                    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.

Found an error in this documentation? The source code for this page can be found here. Please feel free to edit and contribute a pull request.