Peta::FFI::Pool#

📦 std

Run C functions on worker threads, so that a call which hangs cannot hang the program.

Some C calls can block inside the kernel for as long as it likes: statfs, stat or open on an NFS, CIFS or CephFS mount whose server is gone waits uninterruptibly, and no signal or alarm gets the thread back. perl5 programs fork a child with a timeout for every such probe. A pool runs the call on a thread of its own instead, and reports how it went as an event: the event loop stays free, and a hung mount costs one parked thread, not a process per probe.

Peta::FFI::Pool->new(threads => 4) makes a pool of at most four threads. $pool->call($function, $deadline, @args) runs a function prepared with Peta::FFI’s function or attach with @args, and returns the call’s id at once. When the call returns, an event { id => $id, state => 'done', result => ..., errno => ... } is queued, and what the function wrote through its arguments (a P buffer, an *{Class} record, an *i) has been written into the caller’s scalars by the time the event is taken. When $deadline seconds pass first, the event is { id => $id, state => 'timeout' } instead. The thread stays with the call until it returns, since nothing can cancel it, and what the call then produces is thrown away: the caller’s scalars are left alone.

A pool never queues a call. When all its threads are taken, by calls running or by calls hung past their deadline, the call’s event is { id => $id, state => 'busy' }, at once.

The events are taken with next, oldest first, and fd reads ready while there are some, so the pool is a Peta::FFI::EventSource for any event loop.

Synopsis#

use Peta::FFI;
use Peta::FFI::Pool;
use Peta::FFI::EventSource;

my $libc   = Peta::FFI::dlopen("libc.so.6");
my $statfs = Peta::FFI::function($libc, "statfs", "(pP)i");

my $pool   = Peta::FFI::Pool->new(threads => 4);
my $source = Peta::FFI::EventSource->new($pool,
    fd => \&Peta::FFI::Pool::fd, next => \&Peta::FFI::Pool::next);

my $st = "\0" x 120;                    # struct statfs, x86-64
my $probe = $pool->call($statfs, 2, "/mnt/share", \$st);
my $guard = $source->watch(sub {
    for my $event (@_) {
        next unless $event->{id} == $probe;
        if ($event->{state} eq 'done') {
            print $event->{result} == 0 ? "mounted\n" : "error $event->{errno}\n";
        }
        else {
            print "/mnt/share does not answer\n";    # timeout, or busy
        }
    }
});

What a call takes#

A call’s arguments are read and copied before call returns, so the thread never reads a Perl value, and changing them meanwhile changes nothing. A function can take numbers, b, o, p, P, *T of a number or o, handles and records ({Class} and *{Class}). It can return v, a number other than a long double or a complex one, b, o, p, s, a handle or a record. Arrays, the out pointers and arrays of *T[...], *<Class>, *s and *P[...], and closures are refused: a call that needs them runs on Perl’s own thread, with $function->(@args). A closure passed as an o still cannot be called on a pool’s thread, which stops the program as any thread of a library’s own does.

A handle passed to a call is kept alive until the call returns, even past its deadline, so its destructor cannot free what the thread is still using. A handle a late call returns is destroyed, and a string it returns freed; a plain number is lost, so a file descriptor a late open returns stays open.

Threads, signals and fork#

A pool’s threads start when a call needs one, up to threads, and block every signal, which Perl’s thread handles. They belong to the process that made the pool: after fork, the child makes a pool of its own, and call croaks on the parent’s.

Functions#

Pools#

new#

Peta::FFI::Pool->new(threads => $n): a pool of at most $n threads (4 when not given), none started yet.

call#

$pool->call($function, $deadline, @args): run $function, a code reference from Peta::FFI’s function or attach, with @args on one of the pool’s threads, and return the call’s id. Its event is done when it returns, timeout when $deadline seconds (a fraction is fine; undef is no deadline) pass first, and busy at once when every thread is taken.

Events#

fd#

$pool->fd: a descriptor that reads ready while events are queued, for an event loop. Never close it: it is the pool’s.

next#

$pool->next: the oldest event, a hash reference, or undef when there is none. id is the call’s, state is done, timeout or busy, and a done event has the function’s result and the errno the call left; what the function wrote through its arguments is in the caller’s scalars when it is returned.