NAME

Acme::Parataxis::Driver::IOAsync - Drive Parataxis from an IO::Async::Loop

SYNOPSIS

use v5.40;
use Acme::Parataxis qw[await_read await_sleep fiber];
use IO::Async::Loop;

# The usual way: attach the loop and let wrap() choose this driver.
Acme::Parataxis->attach_loop( IO::Async::Loop->new );

Acme::Parataxis::run( sub {
    my $ponger = fiber { await_sleep 30; syswrite $socket, 'ping' };
    await_read( $socket, 2000 );
} );

Acme::Parataxis->detach_loop;

Building one by hand is the same call with the loop named:

my $driver = Acme::Parataxis::Driver::IOAsync->new( loop => IO::Async::Loop->new );

DESCRIPTION

The IO::Async::Loop reference driver. With it attached, await_read, await_write and await_sleep are serviced by IO::Async's loop instead of by an OS worker thread parked in select(). What a caller sees does not change: readiness reports the same values, a deadline still resumes -1, and an enclosing with_timeout still throws. Only the backing mechanism differs.

The loop and Parataxis never run each other's callbacks. IO::Async collects descriptor events, the readiness callback this driver installs only tells the scheduler which fiber to enqueue, and the scheduler hands control back to the loop when every fiber is parked and pending is still true. See Acme::Parataxis::Driver for that division of labour.

loop is an IO::Async::Loop, which is a factory class: IO::Async::Loop->new returns a backend - here IO::Async::Loop::Epoll, and IO::Async::Loop::Select on a platform without epoll, Windows among them. The driver holds whatever it is given and only ever calls watch_io, unwatch_io, watch_time, unwatch_time and loop_once on it, so the backend choice stays IO::Async's.

Unlike the Mojo driver, this one never touches the handle's blocking mode. IO::Async sets O_NONBLOCK itself when a handle is first passed to watch_io, so the end state is the same but the driver is not what did it.

CONSTRUCTOR

new( loop => $loop )

my $driver = Acme::Parataxis::Driver::IOAsync->new( loop => $loop );

loop is a required named parameter and must be a blessed IO::Async::Loop - that is, something that isa it, so a backend class such as IO::Async::Loop::Select is accepted. It croaks otherwise, and the check is on the class rather than on a method list, so an object that merely answers watch_io is not enough.

METHODS

watch_read( $fh, $cb ), watch_write( $fh, $cb )

$driver->watch_read( $fh, sub { $ready->push } );

Register $cb->() for readability or writability on $fh, and return $fh itself - not a boolean and not the descriptor. A closed handle croaks, because there is no descriptor to key the loop on.

Each registration is one watch_io call that re-supplies every direction currently registered for that handle, so the second direction adds the parameter rather than creating a second watch. Registering the same direction twice replaces that callback. A read watch and a write watch on one filehandle still count as a single watch in watch_count, and unwatch takes both down at once.

unwatch( $fh )

$driver->unwatch( $fh );    # true if it was being watched

Drops both directional callbacks for $fh, untracks it, and cancels the loop's watch in a single unwatch_io call that reports both directions - including a direction that was never requested, which IO::Async tolerates. That is what lets one call tear down a read-only or write-only watch, and it works: a read-only watch is fully gone afterwards, and a read event no longer reaches the callback. Returns false when nothing was being watched, so it is safe to call on a handle that was never registered or has already been dropped.

timer( $ms, $cb )

my $id = $driver->timer( 2000, sub { $expired = 1 } );

Arms a loop timer for $ms milliseconds - IO::Async counts in seconds, so the driver divides by 1000 - and returns an opaque id for "cancel_timer( $id )". Treat that id as opaque: it is whatever watch_time hands back, and in IO::Async 0.805 that is a reference.

The id is untracked before the callback runs, so a timer that has fired is no longer cancellable and cancel_timer says so. A delay that is missing, zero, negative or non-numeric croaks.

cancel_timer( $id )

$driver->cancel_timer( $id );    # true if it was still pending

Cancels a still-pending timer and returns true. Returns false when the id is unknown or the timer has already fired.

drive( )

$driver->drive;    # true

Calls $loop->loop_once(undef) and returns true: it blocks in the kernel until an event happens, either I/O readiness or the earliest due timer. This is what run() calls when every fiber is parked and this driver still has something pending.

poll_ready( )

$driver->poll_ready;    # true

Calls $loop->loop_once(0) and returns true: a genuinely non-blocking readiness pass that still fires any timer which has already come due. This is the method the scheduler's polling path can call without stalling on, and it is the one place this driver is more predictable than the Mojo one. See Acme::Parataxis::Driver::Mojo for the contrast.

Behavior Notes

drive( ) on an idle loop never returns

loop_once(undef) with no watchers and no timers waits indefinitely - it does not notice that there is nothing to wait for and hand control back. Measured on an empty loop, repeatedly: drive blocked past a 4-second alarm every time.

This is safe in the shipped path and dangerous by hand. The scheduler only calls drive while pending is true, so it never asks an idle driver to tick. A caller that reaches for drive on its own has no such guard, and will hang rather than get an answer. Reach for poll_ready when you are not certain something is pending.

A descriptor watched both ways reports the writable branch first

A connected socket is always writable, so a loop_once on a handle with both directions registered can report writability and leave the read event for the next pass. This is IO::Async's normal behaviour, not something the driver introduces, and it resolves because the scheduler keeps ticking. Measured on a handle watched both ways with unread data sitting on it: loop_once(0) reported the write callback, and so did the next one.

What was measured, and where

Every timing and callback figure in this file was taken against IO::Async 0.805 on Linux with Perl 5.44.0, where IO::Async::Loop->new returns IO::Async::Loop::Epoll. Nothing here was measured on Windows, where the backend is IO::Async::Loop::Select and the select ceiling described in Acme::Parataxis::Driver applies.

SEE ALSO

Acme::Parataxis::Driver, Acme::Parataxis::Driver::Mojo, Acme::Parataxis, IO::Async::Loop

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.