NAME

Acme::Parataxis::Nursery - Structured Concurrency Container for Scoped Fiber Lifecycles

SYNOPSIS

use v5.40;
use Acme::Parataxis qw[async nursery];

async {
    my ( $user, $preferences );

    my $ok = eval {
        nursery( sub ($n) {
            $n->spawn( sub { $user        = fetch_user(123) } );
            $n->spawn( sub { $preferences = fetch_prefs(123) } );
        } );
        1;
    };
    die $@ unless $ok;

    # Both children are guaranteed to have completed:
    say "User: " . $user->name;
};

DESCRIPTION

Acme::Parataxis::Nursery implements structured concurrency (popularized by Trio and Go errgroups). When concurrent tasks are executed inside a nursery, the lifetimes of all child fibers are bounded by the enclosing block:

  • Guaranteed join: nursery( sub ($n) { ... } ) does not return until every child spawned via $n->spawn has completely terminated.

  • Prompt sibling cancellation: If any child fiber dies with an unhandled exception, the nursery immediately cancels all surviving siblings via an internal cancellation token. The nursery drains all in-flight work before rethrowing the culprit failure.

  • No orphaned fibers: Even if the parent fiber is interrupted (e.g. by an outer with_timeout), all children in the nursery are cancelled and drained before the parent unwinds.

Fibers spawned independently via the top-level fiber { ... } or spawn() inside the block are not enrolled in the nursery and do not delay its join.

METHODS

spawn( sub { ... } )

my $child = $n->spawn( sub {
    do_concurrent_work();
} );

Spawns a child fiber enrolled in this nursery. Returns the Acme::Parataxis fiber object.

Children park once at birth rather than executing synchronously inline. This guarantees that an immediate failure on the child's first statement routes through the scheduler's aggregation machinery rather than throwing unexpectedly into the parent's setup block.

Croaks if called outside a scheduled fiber or without a code reference.

token( )

my $token = $n->token;

Returns the nursery's internal Acme::Parataxis::CancellationToken. Cancelling this token manually cancels all enrolled children and drains the group. Passing this token into a nested with_timeout binds the group to an external deadline.

ERROR HANDLING

Acme::Parataxis::Error::Nursery

When one or more child fibers die, the nursery drains all remaining siblings and throws an Acme::Parataxis::Error::Nursery exception:

  • $err->primary: Returns the first natural failure (the real bug or exception that caused the nursery to fail, ignoring subsequent Cancelled errors thrown by siblings as they were unwound).

  • $err->failures: Returns the list of every exception thrown by any child in the nursery, in spawn order.

use v5.40;
use Acme::Parataxis qw[async nursery await_sleep];

async {
    eval {
        nursery( sub ($n) {
            $n->spawn( sub { die "Database connection lost\n" } );
            $n->spawn( sub { await_sleep(10_000) } );
        } );
    };

    if ( my $err = $@ ) {
        if ( ref $err && $err->isa('Acme::Parataxis::Error::Nursery') ) {
            say "Root cause: " . $err->primary;    # "Database connection lost"
            say "Total child errors: " . scalar( $err->failures ); # 2 (database error + cancelled sleep)
        }
    }
};

Recipes

Concurrent scatter-gather

To collect data from children, write into variables in the parent's lexical scope:

my ( $orders, $inventory, $shipping );

nursery( sub ($n) {
    $n->spawn( sub { $orders    = load_orders($id) } );
    $n->spawn( sub { $inventory = check_stock($id) } );
    $n->spawn( sub { $shipping  = calc_rates($id)  } );
} );

# All three lookups are complete and successful here.

Setting a deadline on the entire nursery

Pass the nursery's token to with_timeout:

use Acme::Parataxis qw[async nursery with_timeout await_sleep];

async {
    eval {
        nursery( sub ($n) {
            $n->spawn( sub { await_sleep(5000) } );
            $n->spawn( sub { await_sleep(5000) } );

            # If 500ms elapses, the token cancels and aborts both children:
            with_timeout( 500, $n->token, sub { await_sleep(10_000) } );
        } );
    };
    warn "Nursery timed out: $@" if $@;
};

Behavior Notes

  • Block returns do not short-circuit children

    The block passed to nursery() cannot return early to bypass pending children. The nursery joins every child spawned via $n->spawn before returning. The nursery's return value is the return value of the enclosed block, delivered only when all children have succeeded.

  • Fail-fast vs. best-effort

    A nursery enforces all-or-nothing execution. The moment one child fails, all surviving siblings are cancelled. If you need independent background tasks where failures do not cascade, use unmanaged fiber { ... } instances combined with Acme::Parataxis::Monitor instead.

  • Parent exceptions take priority

    If the parent block itself throws an exception before spawning finishes, all currently enrolled children are immediately cancelled and drained, and the parent's own exception is rethrown verbatim (not wrapped in an Error::Nursery).

  • Nested nurseries are isolated

    Nesting a nursery inside another forms an independent hierarchy. A failure in an inner nursery cancels only its own children; if the inner nursery's error is caught with an eval, the outer nursery continues uninterrupted.

SEE ALSO

Acme::Parataxis, Acme::Parataxis::CancellationToken, Acme::Parataxis::Error, Acme::Parataxis::Monitor

AUTHOR

Sanko Robinson https://github.com/sanko

LICENSE

Copyright (C) Sanko Robinson.

This library is free software; you can redistribute it and/or modify it under the terms found in the Artistic License 2.