Behaviors as finite state machines

For the Akka Classic documentation of this feature see Classic FSM.

An actor can be used to model a Finite State Machine (FSM).

To demonstrate this, consider an actor which shall receive and queue messages while they arrive in a burst and send them on after the burst ended or a flush request is received.

This example 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
object Buncher {

  // 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
  private case object Timeout extends Event
}
Java
public abstract class Buncher {

  public interface Event {}

  public static final class SetTarget implements Event {
    public final ActorRef<Batch> ref;

    public SetTarget(ActorRef<Batch> ref) {
      this.ref = ref;
    }
  }

  private enum Timeout implements Event {
    INSTANCE
  }

  public enum Flush implements Event {
    INSTANCE
  }

  public static final class Queue implements Event {
    public final Object obj;

    public Queue(Object obj) {
      this.obj = 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.

Scala
sealed trait Data
case object Uninitialized extends Data
final case class Todo(target: ActorRef[Batch], queue: immutable.Seq[Any]) extends Data

final case class Batch(obj: immutable.Seq[Any])
Java
interface Data {}

public static final class Todo implements Data {
  public final ActorRef<Batch> target;
  public final List<Object> queue;

  public Todo(ActorRef<Batch> target, List<Object> queue) {
    this.target = target;
    this.queue = queue;
  }

}

public static final class Batch {
  public final List<Object> list;

  public Batch(List<Object> list) {
    this.list = list;
  }

}

Each state becomes a distinct behavior and after processing a message the next state in the form of a Behavior is returned.

Scala
object Buncher {
  // states of the FSM represented as behaviors

  // initial state
  def apply(): Behavior[Event] = idle(Uninitialized)

  private 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
    }
  }

  private def active(data: Todo): Behavior[Event] =
    Behaviors.withTimers[Event] { timers =>
      // instead of FSM state timeout
      timers.startSingleTimer(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
public abstract class Buncher {
  // FSM states represented as behaviors

  // initial state
  public static Behavior<Event> create() {
    return uninitialized();
  }

  private static Behavior<Event> uninitialized() {
    return Behaviors.receive(Event.class)
        .onMessage(
            SetTarget.class, message -> idle(new Todo(message.ref, Collections.emptyList())))
        .build();
  }

  private static Behavior<Event> idle(Todo data) {
    return Behaviors.receive(Event.class)
        .onMessage(Queue.class, 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.INSTANCE, Duration.ofSeconds(1));
          return Behaviors.receive(Event.class)
              .onMessage(Queue.class, message -> active(data.addElement(message)))
              .onMessage(Flush.class, message -> activeOnFlushOrTimeout(data))
              .onMessage(Timeout.class, message -> activeOnFlushOrTimeout(data))
              .build();
        });
  }

  private static Behavior<Event> activeOnFlushOrTimeout(Todo data) {
    data.target.tell(new Batch(data.queue));
    return idle(data.copy(new ArrayList<>()));
  }

}

To set state timeouts use Behaviors.withTimers along with a startSingleTimer.

Example project

FSM example project FSM example project is an example project that can be downloaded, and with instructions of how to run.

This project contains a Dining Hakkers sample illustrating how to model a Finite State Machine (FSM) with actors.

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.