NAME

Acme::Parataxis::Supervisor - OTP-Style Restart Supervision for Actors and Nested Subtrees

SYNOPSIS

use v5.40;
use Acme::Parataxis qw[async fiber await_sleep];
use Acme::Parataxis::Actor;
use Acme::Parataxis::Supervisor;

async {
    my $sup = Acme::Parataxis::Supervisor->new(
        strategy     => 'OneForOne',    # OneForOne | OneForAll | RestForOne
        max_restarts => 3,
        within       => 30,             # seconds
    );

    # Supervise an actor factory:
    $sup->supervise(
        sub {
            Acme::Parataxis::Actor->spawn(
                sub ( $self, $msg ) {
                    die "Crash requested\n" if $msg eq 'crash';
                    return "Handled: $msg";
                },
                16,
                supervised => 1         # supervised => 1 is required for supervision!
            );
        },
        name => 'worker'
    );

    fiber {
        await_sleep(20);
        say $sup->child('worker')->ask('hello')->await;    # Handled: hello

        # Provoke a crash; the supervisor transparently replaces the worker:
        $sup->child('worker')->send('crash');
        await_sleep(20) until $sup->restarts('worker') > 0;

        say "Restarts observed: " . $sup->restarts('worker');
        say $sup->child('worker')->ask('ping')->await;     # Handled: ping

        $sup->stop;
    };

    $sup->run;    # supervises workers until stop() or restart budget exhaustion
};

DESCRIPTION

While structured concurrency (via Acme::Parataxis::Nursery) provides fail-fast semantics by tearing down a group of fibers on the first error, an Acme::Parataxis::Supervisor implements the Erlang/OTP heal-fast pattern. It runs a supervision fiber that monitors long-lived child workers and automatically restarts them according to a predefined restart strategy when they fail.

Supervised children can be Acme::Parataxis::Actors or nested Acme::Parataxis::Supervisor instances.

Restart Strategies

  • OneForOne (default): When a child dies, only that specific child is restarted. Sibling workers continue running uninterrupted.

  • OneForAll: When any child dies, all supervised children are stopped and restarted. Use this when workers share in-memory state that could be corrupted by a sibling crash.

  • RestForOne: When a child dies, that child and any siblings started after it in registration order are restarted. Earlier workers are left alone.

The Restart Budget

To prevent infinite crash loops when a resource or dependency is permanently broken, restarts are rate-limited: if more than max_restarts occur within within seconds, the supervisor gives up, shuts down all surviving children, and croaks out of run() with an Acme::Parataxis::Error::Supervisor exception.

METHODS

new( [%opts] )

my $sup = Acme::Parataxis::Supervisor->new(
    strategy     => 'OneForOne',
    max_restarts => 5,
    within       => 60,
);

Constructs a new supervisor.

  • strategy: 'OneForOne', 'OneForAll', or 'RestForOne' (default 'OneForOne').

  • max_restarts: Maximum permitted restarts inside the tracking window before the supervisor aborts (default 5).

  • within: Sliding time window in seconds (default 60). Set to 0 for an infinite budget that never trips.

supervise( $factory_or_child, [name => $name] )

$sup->supervise( sub {
    Acme::Parataxis::Actor->spawn( $handler, 16, supervised => 1 );
}, name => 'worker' );

Registers a child under supervision. Returns $self to support method chaining. Must be called before run().

$factory_or_child should ideally be a CODE reference factory returning an Acme::Parataxis::Actor or a nested Supervisor. A factory allows the supervisor to construct a completely fresh instance (with a clean mailbox) upon each restart.

name must be unique within the supervisor; if omitted, defaults to child_1, child_2, etc.

run( )

$sup->run;

Starts the tree, spawns all initial children, and supervises them until stop() is invoked or the restart budget trips. Must be called from inside a scheduled fiber. If the budget is exhausted, shuts down all children and throws Acme::Parataxis::Error::Supervisor.

stop( )

$sup->stop;

Signals the supervisor to shut down. Safe to invoke from any fiber (including from within a child actor's handler). Stops all child instances, drains their teardown hooks, and causes run() to return cleanly.

child( $name )

my $actor = $sup->child('worker');

Returns the live instance currently running for the named child. Returns undef before run() has started. Croaks if $name is unregistered.

children( )

Returns the list of registered child names in start order.

restarts( [$name] )

my $worker_restarts = $sup->restarts('worker');
my $total_restarts  = $sup->restarts;

Returns the restart count for a specific child, or the aggregated total across the entire tree if called without arguments.

running( )

Returns true if run() is actively executing.

strategy( ) / max_restarts( ) / within( )

Read-only configuration accessors.

Recipes

Building a multi-level supervision tree

Supervisors can supervise other supervisors. Inner supervisors heal local failures within their own budget; the outer supervisor only steps in if the entire inner subtree collapses:

async {
    my $db_tree = Acme::Parataxis::Supervisor->new( max_restarts => 2, within => 10 );
    $db_tree->supervise( sub { spawn_db_worker() }, name => 'primary' );

    my root_tree = Acme::Parataxis::Supervisor->new( max_restarts => 5, within => 60 );
    $root_tree->supervise( $db_tree, name => 'database_subsystem' );
    $root_tree->supervise( sub { spawn_api_worker() }, name => 'api' );

    fiber {
        await_sleep(50);
        $root_tree->stop;
    };

    $root_tree->run;
};

Behavior Notes

  • supervised => 1 is required on actors

    By default, an actor swallows exceptions in its message handler and continues running. To enable supervision, actors must be spawned with supervised => 1; this tells the actor to terminate when its handler dies, triggering the supervisor's restart mechanism.

  • Restarts replace instances

    A restarted actor is a completely new object with a fresh mailbox. Callers holding a reference to the old actor object will find it stopped. Retrieve the active instance via $sup->child('name') or use registered named actors.

  • Manual stops count as crashes

    The supervisor detects child termination via death hooks without judging intent. If you call $actor->stop on a supervised child directly, the supervisor interprets this as a termination and restarts it. To permanently retire a child, stop the enclosing supervisor.

SEE ALSO

Acme::Parataxis, Acme::Parataxis::Actor, Acme::Parataxis::Nursery, Acme::Parataxis::Error