Skip to main content

Mailboxes

Every actor has a mailbox — a queue that buffers incoming messages until the actor is ready to process them. MailboxConfig controls capacity and overflow behavior. Every message is wrapped in an Envelope that carries the sender path, target path, and distributed tracing identifiers alongside the payload.

MailboxConfig

MailboxConfig is a final readonly class. Instances come from named constructors.

Unbounded mailbox

The default. No capacity limit; the mailbox grows as needed.

src/Actor/UnboundedExample.php
use Monadial\Nexus\Runtime\Mailbox\MailboxConfig;

$config = MailboxConfig::unbounded();

Bounded mailbox

A fixed-capacity mailbox with a configurable overflow strategy.

src/Actor/BoundedExample.php
use Monadial\Nexus\Runtime\Mailbox\MailboxConfig;
use Monadial\Nexus\Runtime\Mailbox\OverflowStrategy;

$config = MailboxConfig::bounded(1000, OverflowStrategy::DropOldest);

Both withCapacity() and withStrategy() return new MailboxConfig instances — the original is never mutated:

src/Actor/FluentMailboxConfig.php
$config = MailboxConfig::bounded(500)
->withCapacity(1000)
->withStrategy(OverflowStrategy::Backpressure);

OverflowStrategy

The OverflowStrategy enum determines what happens when a bounded mailbox is full and a new message arrives.

StrategyEffect
OverflowStrategy::ThrowExceptionThrow MailboxOverflowException. This is the default.
OverflowStrategy::DropNewestDiscard the incoming message. The mailbox is unchanged.
OverflowStrategy::DropOldestRemove the oldest queued message to make room for the new one.
OverflowStrategy::BackpressureBlock the sender until space is available.

Strategy selection depends on the actor's requirements:

  • ThrowException — fail fast; useful during development or when overflow indicates a design problem.
  • DropNewest — acceptable when the latest data supersedes older data (sensor readings, status updates).
  • DropOldest — acceptable when recent messages must be processed rather than older ones.
  • Backpressure — the sender slows to match the consumer's pace; prevents message loss but may stall upstream actors.

Applying a mailbox configuration

Attach a mailbox configuration to an actor through Props::withMailbox().

src/Actor/BoundedActor.php
use Monadial\Nexus\Core\Actor\Behavior;
use Monadial\Nexus\Core\Actor\Props;
use Monadial\Nexus\Runtime\Mailbox\MailboxConfig;
use Monadial\Nexus\Runtime\Mailbox\OverflowStrategy;

$props = Props::fromBehavior($behavior)->withMailbox(
MailboxConfig::bounded(500, OverflowStrategy::DropOldest),
);

$ref = $system->spawn($props, 'bounded-actor');

When no mailbox configuration is specified, Props::fromBehavior() defaults to MailboxConfig::unbounded().

Envelope

Envelope is a final readonly class that wraps every message with routing information and distributed tracing identifiers.

src/Mailbox/EnvelopeExample.php
use Monadial\Nexus\Core\Mailbox\Envelope;
use Monadial\Nexus\Core\Actor\ActorPath;

$envelope = Envelope::of($myMessage, $senderPath, $targetPath);
PropertyTypeDescription
messageobjectThe actual message payload
senderActorPathPath of the sending actor
targetActorPathPath of the receiving actor
requestIdstring (ULID)Unique identifier for this specific message delivery
correlationIdstring (ULID)Links all messages in the same logical operation
causationIdstring (ULID)Points to the requestId of the message that caused this one
metadataarray<string, string>Arbitrary key-value metadata

Envelope::of() creates a root envelope where all three IDs are set to the same fresh ULID. ActorContext::reply() propagates tracing identifiers automatically. Calling $ref->tell() directly on an ActorRef creates a root envelope and breaks the trace chain.

All modifier methods return a new Envelope — the original is unmodified:

src/Mailbox/EnvelopeModifiers.php
$updated    = $envelope->withMetadata(['key' => 'value']);
$redirected = $envelope->withSender($newSenderPath);

Mailbox interface

The Mailbox interface defines the contract that runtime implementations must fulfill.

src/Mailbox/Mailbox.php
use Monadial\Nexus\Runtime\Mailbox\Mailbox;
use Monadial\Nexus\Runtime\Mailbox\EnqueueResult;
use Monadial\Nexus\Runtime\Duration;

/** @template T of object */
interface Mailbox
{
public function enqueue(object $message): EnqueueResult;
public function dequeue(): mixed;
public function dequeueBlocking(Duration $timeout): object;
public function count(): int;
public function isFull(): bool;
public function isEmpty(): bool;
public function close(): void;
public function isClosed(): bool;
}
  • enqueue() is marked #[NoDiscard] — the EnqueueResult must be inspected. Throws MailboxClosedException if the mailbox has been closed.
  • dequeueBlocking() suspends the current fiber or coroutine until a message arrives or the timeout elapses. Throws MailboxClosedException if the mailbox is closed while waiting.
  • close() permanently shuts down the mailbox. Subsequent enqueue() calls throw MailboxClosedException. Blocked dequeueBlocking() waiters are woken so the actor message loop can exit during system shutdown.

EnqueueResult

ValueMeaning
EnqueueResult::AcceptedMessage successfully added.
EnqueueResult::DroppedMessage discarded (DropNewest or DropOldest strategy).
EnqueueResult::BackpressuredSender blocked until space became available.

Failure modes

Mailbox failures are often silent — dropped messages leave no trace unless you inspect enqueue results or dead letters.

SymptomCauseRecovery
MailboxClosedException thrown on enqueue()Message was sent to an actor whose mailbox is already closed (actor stopped)Check $ref->isAlive() before sending; handle dead letters via $system->deadLetters()
MailboxOverflowException thrownBounded mailbox is full and OverflowStrategy::ThrowException is activeIncrease mailbox capacity, switch to a drop or backpressure strategy, or slow down the sender
MailboxClosedException during dequeueBlocking()The mailbox was closed while the actor was waiting for a messageThis is normal during shutdown; the actor message loop exits cleanly — no action needed
Messages silently disappearDropNewest or DropOldest strategy is active and the mailbox filledInspect EnqueueResult::Dropped from enqueue(); increase capacity or reduce producer rate
StashOverflowException thrownBehavior::withStash() buffer reached its declared capacityIncrease the stash capacity argument, or drain the stash sooner by handling the trigger message earlier

Next steps

  • Actors — how ActorRef::tell() routes messages into the mailbox
  • Ask pattern — how ask() uses the mailbox for request-reply
  • Props — attaching MailboxConfig at spawn time