docs / theater

Contents

Messaging and lifecycle: what actually happens

A reference for how Theater behaves today, read from the source. Use it before designing anything that depends on delivery, ordering, or death notices, so the behavior doesn't have to be found again from the code.

For the design and vocabulary of links and monitors, see actor-relationships-and-lifecycle.md.

1. Message delivery

1.1 The router

All messages, channel opens, and channel messages go through one router task, MessageRouter::router_task. Its command queue holds 10000 (ms:177). It handles one command at a time.

Each actor gets a mailbox of 100 messages when it calls register() (ms:863). Before that, there is no mailbox, and sends to it fail (§1.2). A per-actor task takes messages from the mailbox in order and calls the actor's export. That call then goes through the actor's operation queue (also 100, rt:825), and the actor runs one operation at a time.

The router delivers with mailbox.send(msg).await (ms:281). So when one actor's mailbox is full, the router waits, and no other actor gets messages until that mailbox has room. This is backpressure, not loss. It is a known, deliberately deferred issue.

1.2 send (one-way)

1.3 request (the caller waits for a reply)

1.4 Channels

2. Death

2.1 How actors die

initiate_teardown in rt is the single stop decision. It records one Lifecycle(Terminated { cause }) event on the actor's chain. A second stop is a no-op. The causes are Completed { final_state }, Failed { error }, Stopped, Killed, and PeerKilled { peer }.

2.2 Who hears about it

Nobody, unless they subscribed. The terminal event goes only to the chain's subscribers (§3). There is no default notice to a parent or to the host.

2.3 Lineage

The runtime does not record who spawned whom. GetActors always reports the parent as None (rt:521-528). SubscribeToSpawns (theater/src/messages.rs:379) does carry the parent id, so a host that wants lineage can keep it from that stream. Stopping a parent does not stop its children.

2.4 Restart

There is none. RestartActor (theater/src/messages.rs:310, rt:320) reaches restart_actor (rt:1157), which always returns "Actor restart not implemented". Nothing sends the command. The current direction is to remove it. Restarting is a supervisor spawning again: a new id, starting from its original init value.

3. Watching other actors

3.1 The chain

Every actor has an event chain: export calls, host calls, lifecycle events. The runtime does not keep the events. Each one is hashed, sent to subscribers, and dropped; only the head hash is kept (theater/src/chain/mod.rs:6-10). Anything that needs a record must subscribe and store it.

Delivery to subscribers is strict. Each event is sent with .send().await to each subscriber (theater/src/chain/mod.rs:125), so a slow subscriber slows the actor it watches, and through it anything waiting on that actor.

3.2 Host-side tools

Tool Gives Notes
TheaterRuntime::add_global_subscription(tx) (rt:280) Every chain event of every actor spawned after the call Added as an ordinary chain subscriber (rt:851), so it is strict (§3.1): drain it quickly. theater-cli uses it (theater-cli/src/commands/spawn.rs:179).
TheaterCommand::SubscribeToActor { actor_id, event_tx } (rt:569) One actor's chain, from now on No reply. If the actor isn't running it only logs (rt:575), so the caller can't tell.
TheaterCommand::SubscribeToSpawns (messages.rs:379) One row per spawn, with the parent id

To learn when any actor dies, a host subscribes globally and keeps only events that match theater::subscription::any_termination() (theater/src/subscription.rs:40).

3.3 Actor-side tools (the lifecycle handler)

All three subscribe the caller's handler to the subject's chain and match each event in the host (theater-handler-lifecycle/src/lib.rs).

Call Pattern On a match
link(subject) (l.306) any_termination() (l.317) The caller is stopped with PeerKilled. Directed: A link B stops A when B dies, not the reverse
monitor(subject) (l.328) Pattern::any(): the whole chain (l.339) handle-actor-event(subject, event-type, data) for every event
monitor-filtered(subject, pattern) (l.350) The caller's pattern handle-actor-event for matching events only

To be told when an actor dies, use monitor-filtered(subject, any_termination()). Matching happens in the host, so the watcher's wasm runs once, at the death. The cause is in data, encoded as pack bytes.

A bare monitor is hazardous:

4. Known gaps

5. Keeping this page true

When you change any behavior above, update this page in the same PR. When you learn something about delivery or lifecycle by reading the source, add it here with a file and line, and the commit you checked.