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
undefimmediately 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 actionif 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
donepermanentlyIf the action throws an exception or is cancelled mid-execution, the exception is rethrown in the executing fiber, but
doneis still set to true. All waiting fibers are unblocked (receivingundef), and future calls will not retry. This matches the standard behavior of Go'ssync.Once. If an initialization is allowed to fail and retry, replace theOnceinstance 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, theOnceis 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.