NAME

Acme::Parataxis::Sync::Once - Run an Initialization Action Exactly Once Across Concurrent Fibers

SYNOPSIS

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

my $initializer = Acme::Parataxis::Sync::Once->new;
my $connection;

async {
    my @workers = map {
        my $id = $_;
        fiber {
            # First fiber executes the block; subsequent fibers park until done:
            $initializer->do( sub {
                say "Worker $id initializing shared database connection...";
                $connection = connect_to_db();
            } );

            say "Worker $id proceeding with connection: $connection";
        };
    } 1 .. 4;

    $_->await for @workers;
};

DESCRIPTION

Acme::Parataxis::Sync::Once coordinates lazy, one-time initializations across concurrent fibers. The first fiber to call do executes the provided action; all other concurrent callers park until the action completes and then proceed.

Once the action has completed—whether it returned cleanly, died, or was interrupted—the initializer is marked done. Subsequent calls to do return immediately without re-executing the action.

Result Asymmetry

A key design property: only the fiber that actually executes the action receives its return value. Concurrent waiters and later callers return undef. For this reason, shared initializers should assign their results to an enclosing lexical variable (as shown in the SYNOPSIS) rather than relying on the return value of do().

METHODS

new( )

my $once = Acme::Parataxis::Sync::Once->new;

Constructs a new, unexecuted Once instance.

do( sub { ... } )

my $res = $once->do( sub { initialize() } );

Executes the code reference if it has never run before:

  • First caller: Runs the action synchronously on its own fiber. If the action parks (e.g. for I/O or sleep), the caller parks, and competing fibers queue behind it. Returns the scalar return value of the action.

  • Concurrent callers: Park until the running action finishes, then return undef.

  • Subsequent callers: Return undef immediately without parking.

Exceptions:

  • Croaks if called outside a scheduled fiber.

  • Croaks if passed an invalid code reference.

  • Croaks with Once::do is not reentrant: this fiber is already running the action if called recursively from within the action.

done( )

if ( $once->done ) { ... }

Returns true if the action has completed (or failed); false if unexecuted or currently in flight.

waiters( )

Returns the number of fibers currently parked waiting for the in-flight action to complete.

Recipes

Lazy shared resource setup

my $INIT = Acme::Parataxis::Sync::Once->new;
my $CONFIG;

sub get_config () {
    $INIT->do( sub {
        $CONFIG = load_config_from_disk();
    } );
    return $CONFIG;
}

# Concurrent callers all receive the initialized $CONFIG safely.

Bounded initialization wait

Fibers waiting on a Once can be bounded with with_timeout:

async {
    my $val = eval {
        Acme::Parataxis::with_timeout( 1000, sub {
            $once->do( sub { slow_handshake() } );
        } );
    };
    warn "Timed out waiting for initialization: $@" if $@;
};

Behavior Notes

  • Failed actions mark done permanently

    If the action throws an exception or is cancelled mid-execution, the exception is rethrown in the executing fiber, but done is still set to true. All waiting fibers are unblocked (receiving undef), and future calls will not retry. This matches the standard behavior of Go's sync.Once. If an initialization is allowed to fail and retry, replace the Once instance explicitly upon failure.

  • Re-entrancy spends the Once

    Calling do() recursively from within its own action croaks with a re-entrancy error. Because this is an uncaught exception leaving the action, the Once is finalized as spent, preventing future attempts.

  • Scalar context execution

    The action passed to do() is always evaluated in scalar context. To return multiple values, return an arrayref.

SEE ALSO

Acme::Parataxis::Sync, Acme::Parataxis::Sync::Mutex, Acme::Parataxis::Sync::WaitGroup

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.