NAME
POE::Component::Sequence - Asynchronous sequences with multiple callbacks
SYNOPSIS
use POE qw(Component::Sequence);
POE::Component::Sequence
->new(
sub {
my $sequence = shift;
$sequence->heap_set(
a => 5,
b => 9,
op => '*'
);
},
sub {
my $sequence = shift;
my $math = join ' ', map { $sequence->heap_index($_) } qw(a op b);
$sequence->heap_set(result => eval $math);
}
)
->add_callback(sub {
my ($sequence, $result) = @_;
print "Answer was " . $sequence->heap_index('result') . "\n";
})
->run();
$poe_kernel->run();
DESCRIPTION
A Sequence is a series of code blocks (actions) that are executed (handled) within the same context, in series. Each action has access to the sequence object, can pause it, finish the sequence, add additional actions to be performed later, or store variables in the context (the heap).
If we had the following action in the above example sequence:
sub {
my $sequence = shift;
$sequence->pause;
...
}
...the sequence would pause, waiting for something to call either $sequence->failed, $sequence->finished or $sequence->resume.
Reasoning
Using POE, yield to a state named 'login' with my params
'login' would send a packet along a TCP socket, assigning the state 'login_callback' as the recipient of the response to this packet.
'login_callback' would run with the response
Pass an arbitrary callback to 'login' (which would somehow have to carry to 'login_callback')
Hard code the next step in 'login_callback'
Have 'login_callback' publish to some sort of event watcher (PubSub) that it had logged in
Normally, in Perl when I would create a series of asynchronous steps I needed to complete, I would chain them together using a bunch of hardcoded callbacks. So, say I needed to login to a remote server using a custom protocol, I would perhaps do this:
If I wanted to do something after I was done logging in, I have a number of ways to do this:
The first two mechanisms are cludgy, and don't allow for the potential for more than one thing being done upon completion of the task. While the third idea, the PubSub announce, is a good one, it wouldn't (without cludgly coding) contain contextual information that we wanted carried through the process at the outset. Additionally, if the login process failed at some point in the process, keeping track of who wants to be notified about this failure becomes very difficult to manage.
The elegant solution, in my opinion, was to encapsulate all the actions necessary for a process into a discrete sequence that can be paused/resumed, can have multiple callbacks, and carry with it a shared heap where I could store and retrieve data from, passing around as a reference to whomever wanted to access it.
USAGE
Class Methods
new( ... )
Any method that can be chained on the sequence (add_callback, add_error_callback, and add_finally_callback, for example) can be specified in this arguments hash, but obviously only once, as it's a hash and has unique keys.
Aside from this, the arguments hash is thrown into the $sequence->options and modifies the way the actions are handled (see OPTIONS).
Creates a new Sequence object. Provide a list of actions to be handled in sequence by the handlers.
If the first argument to new() is a HASHREF, it will be treated as arguments that modify the behavior as follows:
Object Methods, Chained
All these methods return $self, so you can chain them together.
add_callback( $subref )
Callbacks are FIFO. Adds the subref onto the list of normal callbacks. See finished()
for how and when the normal callbacks are called. Subref signature is ($sequence, @args || ()) where @args is what was passed to the finished()
call (if the sequence completes without finished()
called, this will be an empty array).
Dying inside a normal callback will be caught, and will move execution to the error callbacks, passing the error message to the error callbacks.
add_error_callback( $subref )
Error callbacks are FIFO. Adds the subref onto the list of error callbacks. See failed()
for how and when the error callbacks are called. Subref signature is ($sequence, @args || ()) where @args is what was passed to the failed()
call (usually a caught 'die' error message).
Return value is not used.
Dying inside an error callback won't be caught by the sequence.
add_finally_callback( $subref )
Adds the subref onto the list of 'finally' callbacks. See finally()
for how and when the 'finally' callbacks are called. This is effectively the same as a normal callback (add_callback()
) but is called even if the sequence ended in failure.
Dying inside a 'finally' callback will not be caught.
add_action( $subref || <some other scalar value> )
Actions are FIFO. Enqueues the given action.
add_handler( $subref )
Handlers are LIFO. Enqueues the given handler. See HANDLERS for more information on this.
add_delay
$sequence->add_delay(
5,
sub {
my $seq = shift;
$seq->failed("Took longer than 5 seconds to process");
# or you can just die and it'll do the same thing
die "Took longer than 5 seconds to process\n";
},
'timeout',
);
Takes $delay, $action and optionally $name. If $name is given and another delay was set with the same name, that delay will be removed and replaced with this new delay. The $action is a subref which will take receive the sequence as it's only argument. The subref will be executed in an eval { }, with errors causing the failure of the sequence.
The return value of the $action subref is usually ignored, but as a special case, if the subref returns [ $POE::Component::Sequence::RUN_AGAIN, $delay ], the same action will be run again after the indicated delay with the same name. This allows you to setup a regular delay without having to do a complex recursive algorithm.
adjust_delay
$sequence->adjust_delay('timeout', 10);
remove_delay
$sequence->remove_delay('timeout');
run()
Starts the sequence. This is mandatory - if you never call run()
, the sequence will never start.
Object Accessors, public
heap(), heap_index(), heap_set(), etc.
heap_index( $key )
Returns the value at index $key
heap_set( $key1 => $value1, $key2 => $value2, ... )
Sets the given key/value pairs, overriding previous values
heap( )
Returns all the key/value pairs of the heap in no particular order
Think of heap()
like the POE::Session heap - it is simply a hashref where you may store and retrieve data from while inside an action. See Class::MethodMaker::hash for all the various heap_* calls that are available to you. The most important are:
options_*()
In usage identical to heap()
above, this is another object hashref. Its values are intended to modify how the handlers perform their actions. See OPTIONS for more info.
alias()
Returns the POE::Session alias for this sequence.
result()
Stores the return value of the last action that was executed. See HANDLERS.
Object Methods, public
pause()
Pauses the sequence
resume()
Resumes the sequence. You must call resume() as many times as pause() was called, as they are cumulative.
finished( @args )
Marks the sequence as finished, preventing further actions to be handled. The normal callbacks are called one by one, receiving ($sequence, @args) as arguments. If the normal callbacks die, execution is handed to failed()
, and then to finally()
.
failed( @args )
Marks the sequence as failed, finishing the sequence. This will happen if an action dies, if failed()
is explicitly called by the user, or if a normal callback dies. The error callbacks are called one by one, receiving ($sequence, @args) as arguments. Afterwards, execution moves to finally()
.
Object Methods, private, POE states
These methods can't be called directly, but instead can be 'yield'ed or 'post'ed to via POE:
$poe_kernel->post( $sequence->alias, 'finally', @args );
finish( @args )
See finished()
.
fail( @args )
See failed()
.
finally( @args )
Walks through each 'finally' callback, passing ($sequence, @args) to each.
next()
The main loop of the code, next()
steps through each action on the stack, handling each in turn. See HANDLERS for more info on this.
OPTIONS
Some options affect the default handler. Other options may be intended for plugin handlers.
auto_pause
Before each action is performed, the sequence is paused.
auto_resume
After each action is performed, the sequence is resumed.
HANDLERS
To make the sequence a flexible object, it's not actually mandatory that you use CODEREFs as your actions. If you wanted to provide the name of a POE session and state to be posted to, you could write a handler that does what you need given the action passed. For example:
POE::Component::Sequence
->new(
[ 'my_session', 'my_state', @args ],
)
->add_handler(sub {
my ($sequence, $request) = @_;
my $action = $request->{action};
if (! ref $action || ref $action ne 'ARRAY') {
return { deferred => 1 };
}
my $session = shift @$action;
my $state = shift @$action;
my @args = @$action;
$sequence->pause;
$poe_kernel->post($session, $state, $sequence, \@args);
# Let's just hope $state will unpause the sequence when it's done...
})
->run;
When an action is being handled, a shared request object is created:
my $request = {
action => $action,
options => \%sequence_options,
}
This request is handed to each handler in turn (LIFO), with the signature ($sequence, $request). The handler is expected to return either a HASHREF in response or throw an exception.
If a handler returns the key 'deferred', the next handler is tried. If the handler returns the key 'skip', the action is skipped. Otherwise, the handler is expected to return the key 'value', which is the optional return value of the $action. This return value is stored in $sequence->result. This value will be overwritten upon each action.
The default handler handles actions only of type CODEREFs, passing to the action the arg $self.
If you'd like to add default handlers globally rather than calling add_handler()
for each sequence, push the handler onto @POE::Component::Sequence::_plugin_handlers. See <POE::Component::Sequence::Nested> for an example of this.
KNOWN BUGS
No known bugs, but I'm sure you can find some.
SEE ALSO
DEVELOPMENT
This module is being developed via a git repository publicly available at http://github.com/ewaters/poe-component-sequence. I encourage anyone who is interested to fork my code and contribute bug fixes or new features, or just have fun and be creative.
COPYRIGHT
Copyright (c) 2008 Eric Waters and XMission LLC (http://www.xmission.com/). All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
The full text of the license can be found in the LICENSE file included with this module.
AUTHOR
Eric Waters <ewaters@gmail.com>