NAME

Acme::Parataxis::Local - Per-Fiber Storage Slots and Context Propagation

SYNOPSIS

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

my $request_id = Acme::Parataxis::Local->new;
my $trace_id   = Acme::Parataxis::Local->new( inherit => 1 );

async {
    $request_id->set(42);
    $trace_id->set('span-100');

    fiber {
        # Inherited slot carries the parent's value at spawn time:
        say "Child trace:   " . $trace_id->get;      # span-100

        # Uninherited slot starts empty (undef):
        say "Child request: " . ($request_id->get // 'unset'); # unset

        $request_id->set(99);
        say "Child modified: " . $request_id->get;    # 99
    };

    # Parent's slot remains untouched:
    say "Parent request: " . $request_id->get;        # 42
};

DESCRIPTION

Acme::Parataxis::Local provides fiber-local storage, analogous to thread-local storage in multi-threaded runtimes. A single Local instance represents a variable that resolves to a private cell for each individual fiber.

Fibers never share or leak cells:

  • Writing to a Local in a child fiber never overwrites the parent's or siblings' values.

  • Unlike Perl's dynamic local, the value follows the fiber's execution lifetime rather than the block scope. It survives any number of await and yield boundaries.

  • When a fiber terminates and is reaped, its local cells are cleaned up automatically, avoiding memory leaks in long-running processes.

Inheritance and Trace Propagation

Constructing a slot with inherit => 1 enables automatic context propagation. When a fiber spawns a child fiber via fiber { ... }, spawn(), an actor, or a nursery, all active inherited slots are shallow-copied into the child's cell before its first statement runs.

This is the standard mechanism for passing request IDs, tracing spans, and security contexts down an asynchronous call tree without manually threading parameters through every helper function. Once copied, the child owns its cell outright; subsequent writes by parent or child do not cross the boundary.

METHODS

new( [%opts] )

my $plain  = Acme::Parataxis::Local->new;
my $seeded = Acme::Parataxis::Local->new( default => 'anonymous' );
my $traced = Acme::Parataxis::Local->new( inherit => 1 );

Constructs a new fiber-local slot. Arguments are keyword-only:

  • default => $val

    The fallback value returned by get() in fibers that have not explicitly set a value. Default is undef.

  • inherit => $bool

    If true, child fibers spawned via spawn(), fiber, nurseries, or actors inherit a snapshot of the parent's current value at spawn time. Default is false.

get( )

my $val = $slot->get;

Returns the value stored in the calling fiber's private cell, or the configured default if unwritten.

Must be called from within a scheduled fiber (croaks with Local-get must be called from inside a scheduled fiber> otherwise).

set( $value )

$slot->set('req-123');

Stores $value in the calling fiber's private cell and returns that value. The write is strictly private to this fiber.

Must be called from within a scheduled fiber (croaks otherwise).

Recipes

Contextual logging without parameter threading

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

my $CURRENT_USER = Acme::Parataxis::Local->new( default => 'guest', inherit => 1 );

sub log_action ($action) {
    my $user = $CURRENT_USER->get;
    say "[$user] $action";
}

async {
    $CURRENT_USER->set('alice');
    log_action('logging in');    # [alice] logging in

    fiber {
        # Inherited context carries into background work:
        log_action('processing queue');    # [alice] processing queue
    };
};

Isolated fiber-scoped resource handles

my $DB_CONN = Acme::Parataxis::Local->new;

sub get_db () {
    my $dbh = $DB_CONN->get;
    unless ($dbh) {
        $dbh = create_database_handle();
        $DB_CONN->set($dbh);
    }
    return $dbh;
}

# Concurrent fibers calling get_db() each maintain an isolated connection.

Behavior Notes

  • Top-level async blocks do not inherit

    Inheritance is wired into spawn(). Because async { ... } and run() initialize a brand-new scheduler run directly rather than spawning off a parent fiber, they do not inherit slots from an enclosing context. Pass values into an async block explicitly or wrap the work in an internal fiber { ... }.

  • Writing undef overrides the default

    default is only consulted when a fiber has never called set(). Calling $slot->set(undef) stores an explicit undefined value, and subsequent get() calls return undef, not the default.

  • Unique slot identity

    Each instantiated Local object receives a monotonically increasing process-wide ID. Two distinct Local instances with identical configurations remain completely isolated from one another.

SEE ALSO

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

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.