NAME
Acme::Parataxis::Actor - A Fiber with a Mailbox
SYNOPSIS
use v5.40;
use Acme::Parataxis qw[async];
use Acme::Parataxis::Actor;
async {
my $upper = Acme::Parataxis::Actor->spawn( sub ( $self, $msg ) {
return uc $msg->{text};
} );
# ask: queue a message and get a future for the handler's answer
say $upper->ask( { text => 'hello' } )->await; # HELLO
# send: queue a message, expect no answer
$upper->send( { text => 'nobody is reading this' } );
# handle what is queued, then shutdown gracefully
$upper->stop;
};
DESCRIPTION
An actor is a fiber with a mailbox. spawn starts one, hands it a handler, and returns immediately. The handler runs in the actor's own fiber, one message at a time, so no other fiber ever executes it and the state the actor owns needs no lock: keep the resource in a closure and reach it with messages.
Two verbs do the talking. ask( $msg ) queues a message and returns a Future that resolves with the handler's return value; send( $msg ) queues one and moves on. stop drains what is queued and ends the actor. Messages are handled in the order they were sent, so answers come back in the order they were asked for, however many fibers are asking at once.
The handler is called as sub ( $self, $msg ) { ... }, so it can use $self to talk back. It is ordinary cooperative code - await_sleep, futures, another actor's ask, a mutex - and the actor waits for it to return before it takes the next message.
The mailbox is a bounded Channel, so a handler slower than its senders pushes back rather than growing a queue. A full mailbox parks the sender, and that is an ordinary fiber wait, so with_timeout and cancellation tokens bound a round-trip like anything else.
All of it belongs inside a scheduled fiber - an async block, a fiber, a nursery child - which is where spawn has to be called from, and where an ask/send that finds the mailbox full parks.
An actor can also be found by name, have its handler replaced while it runs (swap), and be restarted from scratch (respawn, which is what a Supervisor calls).
METHODS
spawn( sub ( $self, $msg ) { ... }, [ $capacity ], %opts )
my $actor = Acme::Parataxis::Actor->spawn( $handler, 16, supervised => 1 );
Starts an actor and returns immediately; it begins taking messages as soon as the scheduler next runs its fiber. Whatever the handler returns is the answer to an ask, and is dropped for a send. $capacity is how many messages the mailbox holds before senders start parking (default 16).
supervised => 1A handler die kills the actor instead of only failing that one message - the failure a supervisor can notice and restart from. See "WHEN A HANDLER DIES".
name => $nameRegisters the actor process-wide so Acme::Parataxis::actor( $name ) can find it.
Croaks outside a scheduled fiber, on a $capacity below 1, on an unknown option, and on a name that another live actor already holds.
ask( $msg )
say $actor->ask( { cmd => 'ping' } )->await; # 'pong'
Queues $msg and returns a Future that resolves with the handler's return value or fails with the handler's error (see "WHEN A HANDLER DIES"). Croaks if the actor is stopping or gone.
send( $msg )
$actor->send( { cmd => 'log' } ); # fire and forget
Queues $msg with no answer expected and returns true once it is queued, parking the calling fiber while the mailbox is full. Croaks if the actor is stopping or gone.
stop( )
$actor->stop; # returns $self
Queues a shutdown behind whatever is already in the mailbox, so that work drains, and then ends the actor's fiber. From the stop on, ask and send croak. Idempotent. To wait for the drain to finish, await a Monitor on the actor:
Acme::Parataxis::Monitor->new($actor)->await; # returns once the actor is really gone
is_alive( )
False once the actor is gone. It stays true while a stop drains, so it answers "is it still going", not "is it done" - see "stop( )" for the way to wait.
on_death( $cb )
$actor->on_death( sub ( $actor, $error ) { warn "worker gone: $error" } );
Registers $cb, which must be a code ref, and returns $self. $cb fires exactly once, as $cb->( $actor, $error ), with $error set to whatever killed the actor or undef for a clean stop. An actor that is already gone calls back immediately, so a watcher never has to check first. This is how a Supervisor learns a child died; to watch an actor you do not own, use a Monitor instead.
error( )
What killed the actor, or undef while it runs and after a clean stop.
name( )
The name it was registered under, or undef for an unnamed actor.
fid( )
The fiber id of the actor's own fiber.
swap( $code )
$actor->swap( sub ( $self, $msg ) { ... } ); # returns $self
Replaces the handler for every message dispatched from now on. The change lands at the next message boundary, with no drain and no restart of the actor: a message already being handled finishes with the old code. Croaks on a non-code ref, and on an actor that is already gone.
respawn( )
my $fresh = $actor->respawn;
A new, already-running actor with the same handler, capacity, and options - supervised and name included, so a restarted actor keeps answering its registry key. It is a new object with a new mailbox rather than a restart of this one, so asks still outstanding against the dead actor fail there instead of being answered here. This is what a supervisor starts in place of a dead child.
THE NAME REGISTRY
Acme::Parataxis->actor( $name ) / Acme::Parataxis->whereis( $name )
my $actor = Acme::Parataxis->actor('billing'); # or Acme::Parataxis::actor('billing')
Returns the handle of the actor spawned with name => $name, or undef when no live actor holds the name. whereis is an alias. The table is process-wide, like Erlang's register table, and only actors given a name are in it. The name is released when the actor stops, dies, or is destroyed, so a dead actor never answers a lookup and the name is reusable afterwards.
A name is also a strong reference, which is the point of it: a named actor stays alive and keeps answering the lookup even after every caller drops its handle. Something has to stop it.
RECIPES
These are very brief demos that should inspire your work.
A round-trip with a deadline
use Acme::Parataxis qw[async with_timeout];
async {
my $reply = $echo->ask( { text => 'hello' } );
my $text = eval { with_timeout( 250, sub { $reply->await } ) } // 'timed out';
};
The message keeps its place in the mailbox when the deadline kills the wait, so the actor is none the wiser.
An actor a supervisor keeps alive
use Acme::Parataxis::Supervisor;
my $sup = Acme::Parataxis::Supervisor->new;
$sup->supervise(
sub { Acme::Parataxis::Actor->spawn( $handler, 16, supervised => 1 ) },
name => 'worker'
);
$sup->run; # restarts 'worker' whenever it dies
supervised => 1 is what turns a handler die into a death the supervisor can see. A restart is a new actor, so re-fetch the handle from $sup->child('worker') or the registry afterwards rather than holding the old one.
A long-lived worker you can find by name
Spawn the long-lived actor once, with a name, and reach it from anywhere in the process - the name is the handle:
Acme::Parataxis::Actor->spawn( $handler, 16, name => 'indexer' );
...
my $indexer = Acme::Parataxis->actor('indexer') or die 'the indexer is not up';
say $indexer->ask( { query => 'parataxis' } )->await;
WHEN A HANDLER DIES
On the default, unsupervised actor, the die is contained: it fails that one ask and nothing else, or - for a send, which has nobody to fail - is reported with warn. Either way the actor carries on with the next message.
With supervised => 1 the die kills the actor instead. The in-flight ask fails with the handler's error, every ask still queued or parked behind it fails with that same error rather than hanging, on_death hooks fire with it, and error reports it. That is the failure a supervisor restarts from.
BEHAVIOR NOTES
Backpressure
A slow handler parks its senders instead of letting the queue grow, so the slowness lands on the senders rather than in memory. That park is an ordinary fiber wait, so
with_timeout, cancellation tokens, and nursery cancellation interrupt it cleanly and the interrupted ask leaves the actor untouched.Stopping
stopqueues its shutdown marker behind the messages already in the mailbox, so those are handled first. A sender that was parked on a full mailbox is not queued yet, though: itssendcan return true with its message dropped, and anaskin that position fails with'actor stopped before this message was handled!'rather than hanging.Talking to yourself
An actor cannot
askitself - the answer it would wait for can only come from the handler that is now waiting for it.sendto yourself, or ask a second actor.State
The handler closure handed to
spawnis the actor's state: whatever it captures lives as long as the actor does, and no other fiber can reach it unless you hand one a reference. Adeferwritten inside a handler runs at the end of that message, not at the end of the actor.
SEE ALSO
Acme::Parataxis, Acme::Parataxis::Supervisor, Acme::Parataxis::Channel, Acme::Parataxis::Future, Acme::Parataxis::Monitor
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.