Actors / Overview

Actors are the building blocks of Pantomime. An actor is a stateful computation abstraction based on the principal of message passing. When an actor receives a message, it may mutate its internal state, spawn child actors, and message other actors.

Concepts

Actor
An actor receives messages and signals, and processes them serially. Upon receipt of a message or signal, it may mutate its own state and spawn other actors. Actors are lightweight -- you can have millions of them on a modest system.
Dispatcher
A dispatcher is responsible for executing thunks of work. For example, this may include executing an actor when it is messaged, or invoking a function that was previously scheduled. An actor may specify a custom dispatcher if it has particular execution requirements. For instance, an actor that performs blocking I/O may wish to be executed on a dispatcher that is reserved for blocking operations, to prevent starving the primary dispatcher.
Mailbox
When an actor is messaged, the message is placed in a mailbox. By default, actors are assigned to a shard to share common infrastructure and reduce memory consumption. An actor may specify a custom mailbox implementation if it wishes to control the delivery semantics, for instance message priority.
Shard
A shard consists of a mailbox and a number of actors. The number of shards is determined at runtime by configuration, but is static in that once the system has been started, the number of shards cannot change. If an actor specifies a custom dispatcher or mailbox, it is placed in its own shard.

Defining Actors

To define an actor, declare a struct with the state that your actor contains, and implement the Actor trait for the message type that your actor can receive.

Receiving Messages
When defining an actor, you must declare the receive method. When an actor is messaged, this method will be called with the message instance.
Receiving Signals
An actor may also define a receive_signal method. This can be used to react to lifecycle notifications. For example, when an actor is spawned, this method will be invoked with a Signal::Started message.
Example

The following example shows a simple actor that maintains a counter. When it receives a GetAndAdd message, it replies with the current value of the counter and then adds the supplied value.

use pantomime::prelude::*;

enum CountingMsg {
  GetAndAdd(u64, ActorRef<u64>)
}

struct CountingActor {
  value: u64
}

impl Actor<CountingMsg> for CountingActor {
  fn receive(&mut self, msg: CountingMsg, _: ActorContext<CountingMsg>) {
    match msg {
      CountingMsg::GetAndAdd(value, reply_to) => {
        reply_to.tell(self.value);
        self.value += value;
      }
    }
  }
}

Creating an ActorSystem

An ActorSystem contains all of the machinery required to spawn and execute actors. This involves spawning a small number of threads for executing the actors (related to available CPUs), a thread to handle I/O events, a thread to generate timer ticks, and a thread to handle actor system events.

A typical application will create an ActorSystem in its main function, spawn any root actors it needs, and then invoke join to start handling system events. For example:

use pantomime::prelude::*;

fn main() {
  // create an ActorSystem
  let mut system = ActorSystem::new();

  // use system.spawn(..) to spawn your root actors, e.g.
  // system.spawn(MyActor);
  // system.spawn(MyOtherActor);

  // use this thread to handle system messages
  system.join();
}

Spawning Actors

Root

Root actors are top-level actors without parents. The only difference from child actors is how they are spawned. Instead of calling spawn on an ActorContext, root actors are spawned by calling spawn on an ActorSystem.

Child

Child actors are spawned from within other actors. To do this, spawn is called on the actor's ActorContext.