NAME
Acme::Parataxis::Compat - Make Blocking Builtins Cooperate (transparent unblocking)
SYNOPSIS
use v5.40;
use Acme::Parataxis qw[async fiber];
# Opt in, in a BEGIN block, above the code you want to change.
BEGIN { Acme::Parataxis->enable_transparent_unblocking() }
async {
my $fh = IO::Socket::INET->new( PeerAddr => 'example.com:80' ) or die $!;
# Plain blocking builtins. Inside a fiber, these are cooperative.
my $napper = fiber { sleep 0.05; say 'f1: slept' }; # no longer stalls the run
my $reader = fiber { my $buf = q{}; read( $fh, $buf, 4 ); say "f2: read '$buf'" };
$napper->await;
$reader->await;
};
DESCRIPTION
Compat makes three blocking builtins cooperative inside a scheduled fiber, so ordinary synchronous code stops freezing everything around it:
sleepbecomes await_sleepThe fiber yields for the time asked for and the other fibers run. Inside a fiber the call returns the duration you asked for, so
sleep 0.05returns0.05-CORE::sleeptruncates that to nothing. Outside the scheduler it isCORE::sleepunchanged, truncation and all. The$_default works.readandsysreadbecome a read framed on await_readThe fiber parks until the scheduler reports the handle readable, then performs one non-blocking read and writes the result back into your scalar. It can hand back fewer bytes than you asked for, so loop when you need an exact count; see "Framing a read of a known length". Waiting on a peer that has not spoken yet is an ordinary fiber wait, so with_timeout and Acme::Parataxis::CancellationToken interrupt it like any other.
Everything else is untouched
select,alarm,timeand the rest still mean exactly what Perl means, andDBIis C underneath and stays blocking. Use await_read and await_write, or a worker thread, for those.
Nothing here happens unless you call Acme::Parataxis->enable_transparent_unblocking(), and even then only code compiled after that call is affected. CORE::GLOBAL overrides are resolved when each piece of code is compiled, so install from a BEGIN block positioned above the code you want to change. Every override delegates to the raw CORE:: builtin outside a scheduled fiber, so top-level code, worker threads, and any region compiled before installation all behave exactly as they always did.
METHODS
Acme::Parataxis->enable_transparent_unblocking( )
BEGIN { Acme::Parataxis->enable_transparent_unblocking() }
Installs the overrides and returns 1. Idempotent, so a second call is not an error, and it takes no arguments. This is the opt-in: until it is called, nothing is patched.
Acme::Parataxis->transparent_unblocking( )
say 'installed' if Acme::Parataxis->transparent_unblocking();
Returns 1 while the overrides are installed and 0 otherwise, including before Compat has ever been loaded. Useful for asserting that a test is really running with them turned on.
Acme::Parataxis->disable_transparent_unblocking( )
Acme::Parataxis->disable_transparent_unblocking();
Restores the saved globs and returns 1. Only code compiled after the call observes the restore, since earlier call sites were already compiled against the override - the same rule that governs installing.
Recipes
Turning it on for one file
package My::App::Server;
use v5.40;
use Acme::Parataxis qw[async fiber];
use Acme::Parataxis::Compat; # optional: enabling loads it for you
BEGIN { Acme::Parataxis->enable_transparent_unblocking() }
sub run { ... } # every sleep and read compiled below here is cooperative
A BEGIN block runs at compile time, in file order, so it has to come after the use lines it depends on and before the code that should be affected. Anything used below the block is compiled after installation and is covered as well; anything already loaded above it is not.
Framing a read of a known length
sub read_exactly {
my ( $fh, $len ) = @_;
my $buf = q{};
my $got = 0;
while ( $got < $len ) {
my $n = read( $fh, $buf, $len - $got, $got ) or last; # 0 at EOF, undef on error
$got += $n;
}
return $buf;
}
A single read is free to return a short count, so a caller that needs an exact number of bytes keeps asking. The OFFSET argument places each chunk at the right spot in the same buffer, and the loop stops on 0 (EOF) or undef (error) instead of spinning. This one is also safe to call from the top level, where read is the raw builtin and the first call usually satisfies it outright.
Running a blocking CPAN module
use v5.40;
use Acme::Parataxis qw[async fiber];
BEGIN { Acme::Parataxis->enable_transparent_unblocking() }
use Net::FTP; # compiled after the install, so its reads and sleeps cooperate
async {
my $ftp = fiber { Net::FTP->new( 'ftp.example.com', Timeout => 30 ) };
say 'other fibers keep running while that works';
$ftp->await;
};
The module cooperates because it was compiled after installation, not because it knows anything about Parataxis. What this does not fix is a module that blocks somewhere the overrides cannot reach - a connect, a C-level call like DBI, or its own hand-rolled select loop. Those still park the whole OS thread, and the answer there is await_read or a worker thread.
Turning it off for one test
subtest 'sleep truncates again without the overrides' => sub {
Acme::Parataxis->disable_transparent_unblocking();
my $rc = eval 'sleep(0.05)'; # freshly compiled, so it sees the restore
ok $rc == int $rc, 'raw CORE::sleep truncated the fraction';
Acme::Parataxis->enable_transparent_unblocking();
};
The restore only reaches code compiled after it, which is why the snippet has to be evaled rather than calling something already compiled above.
Behavior Notes
A short read is not an error
readandsysreadreturn whatever has arrived, which on a socket is routinely less than the count you asked for. A raw blockingreadon a stream handle would have kept waiting for the remainder, but that wait belongs to the OS thread and not to the fiber, so nothing else in the program could run and no deadline or token could interrupt it. The count you get back is the honest one; looping for more is the caller's job.Waiting is still interruptible
A read parked on a handle with nothing to say parks in the scheduler, so with_timeout, a CancellationToken, or a nursery failure all cut it short. It is the read itself that can now come up short, never the wait in front of it.
Your handle is handed back as you gave it
The override switches a handle to non-blocking for the duration of its own read and restores the flags before returning, so code compiled before installation - which still gets a raw blocking read from that same handle - does not start failing with
EAGAIN. A caller that setO_NONBLOCKitself gets its handle back that way too, and a read at the top level never touches the mode at all.Buffered and careful reads differ
readon aPerlIO-buffered handle takes whatever the buffer already holds and only reaches for the descriptor if it has to, so it can return a short count drawn entirely from memory.sysreadalways goes to the descriptor. Both are framed on readiness the same way.Only code compiled after you enable it is affected, and bareword handles stop parsing
An override is installed, not compiled in, so it changes the parser's mind for every
readandsysreadthat is compiled afterwards. What the parser loses is the builtin's special case for a bareword handle in first position, so underuse strictthis stops compiling once the override is in place:use strict; use Acme::Parataxis; BEGIN { Acme::Parataxis->enable_transparent_unblocking() } require Some::Module; # dies: Bareword "FH" not allowed while "strict subs" in useA lexical handle -
open my $fh- is unaffected, and so is any module already loaded, which is the other half of the same rule:requirea module that reads from a bareword handle before you enable. This is a compile-time failure, sono warningswill not quiet it.Reading a regular file keeps working, with or without an attached loop:
await_readis aselect()on the built-in pool, and a reactor that cannotepoll_ctla file reports it as always ready instead, which is what IO::Async::Loop does onEPERM.Only three builtins
There is no override for
select,alarm,time,connect, or anything written in C - and nothing is stubbed to warn you at the call site either. An uncovered blocking call simply blocks, which is one reason the install is opt-in and off by default.Nothing surprising when disabled
disable_transparent_unblockingrestores the saved globs exactly. Only freshly compiled code observes the restore, since earlier call sites were already compiled against the override.
SEE ALSO
Acme::Parataxis (await_sleep, await_read, await_write, with_timeout), Acme::Parataxis::Error, Acme::Parataxis::CancellationToken, Acme::Parataxis::Nursery
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.