Behaviors as finite state machines

You are viewing the documentation for the new actor APIs, to view the Pekko Classic documentation, 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
sourceobject 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
sourcepublic 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
sourcesealed 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
sourceinterface 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
sourceobject 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 =>
    (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
sourcepublic 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<>()));
  }

}

The method idle above makes use of Behaviors.unhandled which advises the system to reuse the previous behavior, including the hint that the message has not been handled. There are two related behaviors:

  • return Behaviors.empty as next behavior in case you reached a state where you don’t expect messages any more. For instance if an actor only waits until all spawned child actors stopped. Unhandled messages are still logged with this behavior.
  • return Behaviors.ignore as next behavior in case you don’t care about unhandled messages. All messages sent to an actor with such a behavior are simply dropped and ignored (without logging)

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.