NAME
Acme::Parataxis::Channel - Bounded Message Queues and CSP Multiplexing for Fibers
SYNOPSIS
use v5.40;
use Acme::Parataxis qw[async fiber];
use Acme::Parataxis::Channel;
my $log = Acme::Parataxis::Channel->new( capacity => 1024 );
async {
# Producer: parks while the channel is full
fiber {
$log->put($_) for 1 .. 8;
$log->shutdown;
};
# Consumer: parks while the channel is empty; stops on EOF (undef)
while ( defined( my $msg = $log->get ) ) {
say "Received: $msg";
}
};
DESCRIPTION
A channel is a buffer with two ends and a capacity. put appends to the tail, get removes from the head, and both ends can be shared across any number of concurrent fibers.
Capacity drives backpressure: when the buffer is full, producers park; when it is empty, consumers park. A slow reader naturally pushes back on a fast writer, preventing unbounded queue growth in memory. Setting capacity => 1 creates a rendezvous channel: a put will only complete once a get is actively waiting to take the value.
Every park here is a standard fiber wait. with_timeout and Acme::Parataxis::CancellationToken interrupt a get or put cleanly: an interrupted put leaves nothing in the channel, and an interrupted get leaves the next queued item for subsequent readers.
Calling shutdown closes the write side. Any items already buffered remain available to get. Once drained, subsequent get calls return undef immediately without parking. This is the canonical way to signal EOF down a pipeline and allows Acme::Parataxis::Stream stages to unwind cleanly.
Channels serve as the internal substrate across Parataxis: an Actor mailbox is a channel, a Ticker publishes ticks through a capacity-1 channel, and a Supervisor aggregates worker crashes through one.
METHODS
new( [%opts] )
my $q = Acme::Parataxis::Channel->new; # effectively unbounded
my $q = Acme::Parataxis::Channel->new( capacity => 1024 ); # bounded: put parks when full
my $q = Acme::Parataxis::Channel->new( capacity => 8, timeout => 200 );
Named arguments only:
capacity => $intBuffer slot limit before
putparks (default:2_000_000_000, effectively unbounded). Must be at least1. If you want backpressure, always set this explicitly.timeout => $msDefault wait bound in milliseconds for any
getorputthat parks. If the wait exceeds this duration, it throws Acme::Parataxis::Error::Timeout. Default0(no timeout).
get( )
my $val = $q->get;
Removes and returns the oldest value in the channel, parking the calling fiber if the channel is empty. Once a shut-down channel has been fully drained, get returns undef forever. Honors the channel's configured timeout.
put( $value )
$q->put($msg);
Appends $value to the tail, parking the calling fiber if the channel is full. If a deadline or token cuts the wait short, the item is not queued. Honors the channel's configured timeout.
try_get( )
my ( $ok, $val ) = $q->try_get;
Non-blocking read. If an item is present, removes it and returns (1, $val). If empty, returns (0, undef) immediately without parking or arming timers.
try_put( $value )
$q->try_put($msg) or warn "Buffer full, dropped";
Non-blocking write. If room is available, enqueues $value and returns true. If full, returns false immediately without parking.
put_priority( $value )
$q->put_priority($urgent_msg);
Enqueues $value immediately without ever parking, even if the channel is already at or above capacity. Used for out-of-band control signals (such as an Actor's shutdown sentinel) and safe to call from destructors or contexts where yielding is prohibited.
size( )
Returns the number of unread items currently buffered.
capacity( )
Returns the initial capacity configured during new.
timeout( )
Returns the configured default timeout in milliseconds (0 if none).
shutdown( )
$q->shutdown;
Closes the channel to future writes. Wakes all fibers currently waiting in get() or in a select read case. Buffered items remain accessible; once empty, all subsequent reads yield undef. Idempotent.
adjust( $diff )
$q->adjust(100); # expand capacity by 100
$q->adjust(-50); # contract capacity by 50
Adjusts available write capacity by $diff and returns true. If shrinking causes capacity to fall below the current size(), existing buffered items remain intact, but put will park until the queue drains below the new threshold.
select_waiters( )
Returns the number of fibers currently parked in a select call involving this channel.
remove_waiter( $fid )
$q->remove_waiter($fid);
Unregisters a fiber ID from the channel's internal get, put, and select wait queues, returning the count of entries removed. Used during park interruption cleanup.
CSP MULTIPLEXING: select
Acme::Parataxis::Channel->select( @cases, [%opts] )
my ( $ch, $val ) = Acme::Parataxis::Channel->select(
[ $inbox, 'get' ],
[ $outbox, 'put', $reply ],
timeout => 1_000,
default => sub { 'fallback' }
);
A class method that blocks until one of several channel operations can proceed. Probes cases in randomized order to prevent starvation. If an operation is ready immediately, it commits without yielding.
Cases
Each case is an arrayref:
[ $channel, 'get' ]- ready when$channelhas a readable value.[ $channel, 'put', $value ]- ready when$channelhas capacity to accept$value.
Options
timeout => $msBounds the wait. If the timeout expires before any case is ready,
selectreturns( undef, undef )rather than throwing. If omitted, the shortest positivetimeoutamong the involved channels is used.default => sub { ... }If supplied,
selectwill never park. If no channel is immediately ready, it runs the callback and returns( undef, $default_return ).
Recipes
Draining a queue to EOF
my $queue = Acme::Parataxis::Channel->new( capacity => 64 );
fiber {
while ( defined( my $item = $queue->get ) ) {
process_item($item);
}
say 'All items drained';
};
produce_into($queue);
$queue->shutdown;
Because EOF is signaled as undef, avoid sending raw undef values through the channel; wrap them in arrayrefs or objects if undefined payloads are legitimate data.
Capacity-1 rendezvous
my $req_ch = Acme::Parataxis::Channel->new( capacity => 1 );
my $resp_ch = Acme::Parataxis::Channel->new( capacity => 1 );
fiber {
my $req = $req_ch->get;
$resp_ch->put( handle_request($req) );
};
$req_ch->put($payload);
my $reply = $resp_ch->get;
Multiplexing work and heartbeats
while (1) {
my ( $ch, $val ) = Acme::Parataxis::Channel->select(
[ $jobs, 'get' ],
[ $ticker, 'get' ],
timeout => 5000,
);
if ( !defined $ch ) {
warn "No activity for 5 seconds";
}
elsif ( $ch == $jobs ) {
last if !defined $val; # jobs channel was shut down
handle_job($val);
}
elsif ( $ch == $ticker ) {
send_heartbeat();
}
}
Behavior Notes
EOF semantics across methods
When a channel is shut down and drained,
getreturnsundef,try_getreturns(1, undef), and aselectget case returns($channel, undef). Check for defined values when reading from channels that may close.Backpressure is a park
When a bounded channel fills, producers park inside the scheduler rather than consuming system memory. This makes cancellation safe: an interrupted
putsimply aborts its registration, leaving the buffer untouched.Timeouts apply only to parking operations
A
getorputthat succeeds immediately never arms a timer. Neithertry_getnortry_putever consults the configured timeout.
SEE ALSO
Acme::Parataxis, Acme::Parataxis::Stream, Acme::Parataxis::Actor, Acme::Parataxis::CancellationToken
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.