NAME
Acme::Parataxis::Driver - Base Class for Event-Loop Adapters Driving Parataxis
SYNOPSIS
use v5.40;
use Acme::Parataxis qw[await_read await_sleep fiber];
use Mojo::IOLoop;
# Attach an event loop. Parataxis wraps it in the matching reference driver:
Acme::Parataxis->attach_loop( Mojo::IOLoop->new );
Acme::Parataxis::run( sub {
my $reader = fiber { await_read( $socket, 2000 ) }; # an I/O watch on the loop
my $napper = fiber { await_sleep(50) }; # a timer on the loop
...
} );
Acme::Parataxis->detach_loop; # unwinds all watches/timers; reverts to C thread pool
DESCRIPTION
A driver is the adapter between Parataxis and an existing CPAN event loop. Attaching a loop via Acme::Parataxis->attach_loop switches await_read, await_write, and await_sleep away from submitting jobs to the background OS worker pool. Instead, readiness and timers are registered directly with the attached loop.
The immediate benefit is multiplexing via epoll, kqueue, or reactor notification rather than dedicating a C thread parked in select() per filehandle. This also lifts the traditional FD_SETSIZE ceiling on platforms where the event loop supports modern poll interfaces.
The division of labor is strictly isolated:
The driver manages loop registrations: it tells the event loop which handles to watch and which timers to arm, and hands control to the loop when all fibers are parked.
The scheduler retains complete ownership of fibers, parks, and resumes. Loop callbacks only enqueue fibers back onto the run queue; they never execute user code directly. This prevents re-entrancy into the scheduler from inside third-party event loop ticks.
Parataxis and the event loop never invoke each other's user callbacks—they cooperate exclusively through readiness triggers.
From the perspective of a running fiber, the API contract is unchanged: await_read and await_write return 1 on readiness, -1 on timeout or unwatchable descriptor, and enclosing with_timeout or nursery scopes abort cleanly.
Two reference drivers ship with the distribution: Acme::Parataxis::Driver::Mojo (for Mojo::IOLoop and Mojo::Reactor) and Acme::Parataxis::Driver::IOAsync (for IO::Async::Loop). Acme::Parataxis::Driver::wrap selects between them automatically.
METHODS
pending( )
my $driver = Acme::Parataxis->loop; # undef unless attached
say $driver->pending; # watch_count + timer_count
Returns the total number of watches and timers currently managed by this driver. The scheduler idle branch only yields control to the loop when pending is greater than zero; if nothing is pending, the scheduler proceeds to deadlock detection rather than stalling indefinitely inside the loop.
reset( )
$driver->reset;
Unwinds and cancels every watch and timer registered with the loop, clearing internal tracking tables. Automatically invoked when run() terminates and during detach_loop(). This guarantees that leftover listeners or orphaned timers from an exited run cannot stall subsequent scheduler runs. Returns true.
has_watch( $fh )
if ( $driver->has_watch($fh) ) { ... }
Returns true if $fh is currently being watched for read or write readiness. Both directions share a single watch slot per handle.
Slots are keyed by descriptor number but validated by handle identity (via builtin::refaddr). Because operating systems eagerly recycle descriptor numbers when handles close, matching on filehandle identity prevents a newly opened handle from accidentally inheriting stale callbacks associated with a retired descriptor number.
watch_count( )
Returns the number of filehandles currently registered for readiness monitoring.
timer_count( )
Returns the number of timers currently armed on the loop.
Writing a Custom Driver
Subclasses implement the loop-specific interactions. The base class manages the bookkeeping table (which handles and timer IDs are active) relied on by pending() and reset().
Custom drivers should inherit from Acme::Parataxis::Driver and use the internal tracking helpers: _track_watch, _untrack_watch, _track_timer, and _untrack_timer.
package My::Driver {
use v5.40;
use feature 'class';
no warnings 'experimental::class';
class My::Driver : isa(Acme::Parataxis::Driver) {
field $loop : param;
method watch_read ($fh, $cb) {
my $fd = fileno($fh) // die 'Cannot watch a closed handle';
$loop->add_reader( $fh, sub { $cb->() } );
$self->_track_watch($fh);
}
method watch_write ($fh, $cb) { ... }
method unwatch ($fh) { ... } # loop remove + _untrack_watch
method timer ($ms, $cb) { ... } # loop timer + _track_timer
method cancel_timer ($id) { ... } # loop cancel + _untrack_timer
method drive () { ... } # run one blocking loop tick
method poll_ready () { ... } # run one non-blocking tick
}
};
Subclasses must implement:
watch_read( $fh, $cb )- Fire$cb->()when$fhis readable. Croaks if closed.watch_write( $fh, $cb )- Fire$cb->()when$fhis writable. Croaks if closed.unwatch( $fh )- Stop watching both directions on$fh. Return false if unmonitored.timer( $ms, $cb )- Arm a timer to invoke$cb->()after$msmilliseconds. Returns an opaque ID.cancel_timer( $id )- Cancel an active timer. Returns false if already expired or unknown.drive( )- Execute the loop until at least one event fires (may block).poll_ready( )- Perform a single non-blocking readiness pass and return immediately.
FUNCTIONS
Acme::Parataxis::Driver::wrap( $loop )
my $driver = Acme::Parataxis::Driver::wrap( $loop );
Wraps an event-loop instance in the appropriate reference driver. Returns the driver object as-is if already blessed into an Acme::Parataxis::Driver subclass.
Mojo::IOLooporMojo::Reactorinstances wrap as Acme::Parataxis::Driver::Mojo.IO::Async::Loopinstances wrap as Acme::Parataxis::Driver::IOAsync.
Croaks if passed an unblessed value or an unsupported loop class.
Recipes
Attaching for a single scoped run
Keep driver attachments scoped: attach before running, and detach when finished:
my $old_driver = Acme::Parataxis->attach_loop( Mojo::IOLoop->new );
Acme::Parataxis::run( sub {
# driven via epoll/kqueue through Mojo
} );
Acme::Parataxis->detach_loop; # reverts cleanly to native worker pool
Attaching or detaching while a scheduler run is active croaks to prevent corrupting in-flight watches.
Driving an existing embedded Mojo::Reactor
You do not need a full Mojo::IOLoop; any object implementing the Mojo::Reactor interface works:
my $reactor = $ioloop->reactor;
Acme::Parataxis->attach_loop($reactor);
Acme::Parataxis::run( sub {
await_sleep(50);
} );
Behavior Notes
Platform descriptor ceilings
When a loop relies on select(), descriptor limits are bound by the platform C library's FD_SETSIZE:
Linux, macOS, FreeBSD: 1024 (128-byte bitmask)
NetBSD: 256 (32-byte bitmask)
Windows: 64-entry socket array
Because FD_SETSIZE bounds the numeric value of the descriptor rather than the total count of active handles, twenty descriptors opened early and three hundred opened late occupy different watch ranges.
Parataxis inspects descriptors before registration. Descriptors at or exceeding FD_SETSIZE are rejected immediately with -1 (simulating a timeout) rather than writing past the end of a C library bitmask.
poll_ready( ) semantics vary by backend
The scheduler calls poll_ready() when background C pool tasks remain in flight and runnable fibers are temporarily exhausted.
Acme::Parataxis::Driver::IOAsync executes
$loop->loop_once(0), returning instantly while still firing due timers.Acme::Parataxis::Driver::Mojo uses
$reactor->one_tick, which ignores duration arguments and always waits for the earliest pending timer. A long timer combined with an active C worker task can causepoll_ready()to wait for that timer before servicing the worker completion.
SEE ALSO
Acme::Parataxis, Acme::Parataxis::Driver::Mojo, Acme::Parataxis::Driver::IOAsync
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.