NAME

Acme::Parataxis::Compat - Transparent Gevent-Style Cooperative Unblocking of Builtins

SYNOPSIS

use v5.40;
use Acme::Parataxis qw[async fiber];

# Must be installed at compile time, in a BEGIN block, above affected code:
BEGIN { Acme::Parataxis->enable_transparent_unblocking() }

async {
    my $sock = IO::Socket::INET->new( PeerAddr => 'example.com:80' ) or die $!;

    # Standard blocking builtins are now cooperative inside fibers:
    my $napper = fiber {
        sleep 0.05;    # yields to the scheduler; does not block the OS thread
        say 'f1: finished sleep';
    };

    my $reader = fiber {
        my $buf = q{};
        read( $sock, $buf, 64 );    # frames onto await_read; yields while waiting
        say "f2: read ${\length($buf)} bytes";
    };

    $napper->await;
    $reader->await;
};

DESCRIPTION

Acme::Parataxis::Compat provides an opt-in, gevent-style compatibility layer for legacy synchronous code. By installing CORE::GLOBAL overrides for sleep, read, and sysread, existing modules and synchronous loops run cooperatively inside fibers without freezing the entire process:

  • sleep becomes await_sleep

    The calling fiber yields for the requested duration while other fibers proceed. Fractional seconds are honored with millisecond precision (sleep 0.05 sleeps 50ms, whereas CORE::sleep truncates this to 0). When called outside a fiber, it falls back immediately to CORE::sleep. The $_ default argument is supported.

  • read and sysread become non-blocking reads framed on await_read

    The fiber parks until the filehandle is reported readable, flips the handle non-blocking, reads available bytes, and restores the original handle mode. It can return fewer bytes than requested, exactly like a non-blocking socket read; callers needing an exact byte count should loop.

  • Everything else remains untouched

    select, alarm, connect, and C-level extensions like DBI are not intercepted and remain blocking. Use await_read, await_write, or Acme::Parataxis::Blocking for those.

Nothing is modified unless you explicitly invoke enable_transparent_unblocking(), and only code compiled after the call is affected. Perl resolves CORE::GLOBAL overrides at parse time, so installation must take place within a BEGIN block placed above the target code. Outside of a running fiber, all three overrides delegate directly to the native CORE:: builtins.

METHODS

Acme::Parataxis->enable_transparent_unblocking( )

BEGIN { Acme::Parataxis->enable_transparent_unblocking() }
# or:
BEGIN { Acme::Parataxis::Compat->install() }

Installs the CORE::GLOBAL overrides and returns 1. Idempotent.

Acme::Parataxis->disable_transparent_unblocking( )

Acme::Parataxis->disable_transparent_unblocking();
# or:
Acme::Parataxis::Compat->disable();

Restores the global symbols to their pre-installation state. Safely clears internal subroutine depth tracking so Perl's subroutine finalization does not trigger "Can't undef active subroutine" errors. Only code compiled after this call reverts to the builtins.

Acme::Parataxis->transparent_unblocking( )

say 'Active' if Acme::Parataxis->transparent_unblocking();
# or:
say 'Active' if Acme::Parataxis::Compat->installed();

Returns true if the transparent unblocking overrides are currently installed.

Recipes

Enabling unblocking for a single file or script

package My::Worker;
use v5.40;
use Acme::Parataxis qw[async fiber];

BEGIN { Acme::Parataxis->enable_transparent_unblocking() }

use Net::FTP;    # compiled AFTER the overrides; its internal reads and sleeps cooperate!

async {
    my $ftp = fiber { Net::FTP->new('ftp.example.com', Timeout => 10) };
    $ftp->await;
};

Reading an exact number of bytes safely

Because non-blocking reads return as soon as data arrives, wrap reads in a loop when a fixed payload size is required:

sub read_exact ( $fh, $len ) {
    my $buf = q{};
    my $got = 0;
    while ( $got < $len ) {
        my $n = read( $fh, $buf, $len - $got, $got );
        last if !defined($n) || $n == 0;    # error or EOF
        $got += $n;
    }
    return $buf;
}

The OFFSET parameter places each chunk at the correct offset in the buffer without extra string copies.

Scoped testing and cleanup

use Test2::V1;
use Acme::Parataxis;

subtest 'sleep restores truncation outside overrides' => sub {
    Acme::Parataxis->disable_transparent_unblocking();
    my $rc = eval 'sleep(0.05)';    # eval forces compilation after disable()
    is $rc, 0, 'raw CORE::sleep truncated fractional seconds';
    Acme::Parataxis->enable_transparent_unblocking();
};

Behavior Notes

  • Short reads are normal

    Perl's builtin read() blocks the entire OS thread until the requested buffer is completely filled. Compat::read cannot do this without freezing sibling fibers. Instead, it waits for readiness and returns whatever bytes have arrived. Always loop if your protocol requires a fixed frame length.

  • Waiting remains fully cancellable

    Waiting for a handle to become readable parks the fiber in the scheduler. Deadlines from with_timeout, cancellations from Acme::Parataxis::CancellationToken, and nursery abort signals will interrupt the wait cleanly.

  • Original handle modes are restored

    The override switches the filehandle to non-blocking mode (via IO::Handle::blocking($fh, 0)) only for the duration of the read, and restores the original mode before returning. Code compiled before installation that shares the handle continues to observe standard blocking semantics.

  • Bareword filehandles stop compiling

    Because CORE::GLOBAL overrides cannot reproduce the compiler's specialized grammar rules for bareword filehandles, code compiled under use strict after installation will fail to compile bareword handles:

    BEGIN { Acme::Parataxis->enable_transparent_unblocking() }
    read( FILEHANDLE, $buf, 128 );    # syntax error under strict subs

    Always use lexical filehandles (my $fh) or indirect handles (\*STDIN) in code compiled with unblocking enabled.

  • Windows non-socket fallback

    Windows select() only operates on network sockets; attempting to wait on a pipe or disk filehandle returns an immediate error. When Compat detects a descriptor on Windows that completes its probe in less than 90% of the wait budget, it recognizes the handle as unwatchable and performs an immediate direct read. If a pipe has no data buffered, this direct read will block the host thread until data arrives.

SEE ALSO

Acme::Parataxis, Acme::Parataxis::CancellationToken, IO::Handle

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.