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:

  • sleep becomes await_sleep

    The 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.05 returns 0.05 - CORE::sleep truncates that to nothing. Outside the scheduler it is CORE::sleep unchanged, truncation and all. The $_ default works.

  • read and sysread become a read framed on await_read

    The 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, time and the rest still mean exactly what Perl means, and DBI is 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

    read and sysread return whatever has arrived, which on a socket is routinely less than the count you asked for. A raw blocking read on 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 set O_NONBLOCK itself 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

    read on a PerlIO-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. sysread always 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 read and sysread that is compiled afterwards. What the parser loses is the builtin's special case for a bareword handle in first position, so under use strict this 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 use

    A lexical handle - open my $fh - is unaffected, and so is any module already loaded, which is the other half of the same rule: require a module that reads from a bareword handle before you enable. This is a compile-time failure, so no warnings will not quiet it.

    Reading a regular file keeps working, with or without an attached loop: await_read is a select() on the built-in pool, and a reactor that cannot epoll_ctl a file reports it as always ready instead, which is what IO::Async::Loop does on EPERM.

  • 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_unblocking restores 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.