develooper Front page | perl.module-authors | Postings from June 2008

Module proposal: event hook lists

Thread Next
From:
Paul LeoNerd Evans
Date:
June 3, 2008 04:50
Subject:
Module proposal: event hook lists
Message ID:
20080603124957.24b803ea@nim.leo
[The following is a large collection of notes and ramblings I've been
thinking on... any comments/thoughts would be appreciated]

So... Hook lists. They're a source of events.

They're named. Events have arguments, and perhaps return values. Simple events ignore their returns. More complex ones might do fancier things. More on this later.

Event arguments are passed by-reference, so handlers get the chance to mangle them, and the modifications visible to later handlers.

Modules can hook into these and insert their own handlers. Each event might have lots of handlers. So ordering becomes a problem. More on this later too.

Handler functions might be synchronous, might be async. They might want to run forever, they might want to be removed at some time. Maybe even we'll want to remove one ahead-of-time.

So what's our API look like? Does the "hook list" exist as a firstclass object? It might want to... Or an object that has hooklists might be in a role, that supports them by names.

So our basic registration API might look like one of the following:

  $hooklist->register_sync( \&my_handler );
  $hooklist->register_async( \&my_async_handler );

  $object->register_hook_sync( "event", \&my_handler );
  $object->register_hook_async( "event", \&my_async_handler );

With handlers that looked like:

  sub my_handler
  {
     my ( $event, @args ) = @_;
     print "$event happened with @args\n";
     return;
  }

  sub my_async_handler
  {
     my %params;
     my $event = $params{event};
     my @args  = @{ $params{args} };

     print "$event happened with @args\n";

     # Typically this would be called sometime later; it invokes a 'next'
     # continuation that was passed in
     $params{next}->();  
  }

We can call them something like:

  $hooklist->fire( @args );     # Would only work if there's no async. ones
  $hooklist->fire_async( args => \@args, on_done => sub { .... } );

  $object->fire_hook( "event", @args ); # Ditto
  $object->fire_hook_async( event => "event", args => \@args, on_done => sub { .... } );

So what about hooks to be removed? Synchronous ones can just return some value to say if they want removing or not:

   sub my_first_handler
   {
      ...
      return $hang_around ? 1 : 0;
   }

What about async. ones? They might not know yet by the time the function returns if they want to persist or not. So maybe we give them an ID and some way to remove themself?

  sub my_second_handler
  {
     my %params = @_;
     my $id = $params{id};
     my $hooklist = $params{hooklist};

     ...

     later( sub {
        $hooklist->unregister_hook( $id ) if not $hangaround;
        $params{next}->();
     } );
  }

At which point, we have enough of an API to actually consider making the synchronous ones do that too, and have the registration functions return the ID

  my $id = $hooklist->register_sync( \&my_handler );

  $hooklist->unregister( $id ) if $cancel;

  sub my_handler
  {
     my %params = @_;    # Make the API look the same as for sync. ones

     my $id       = $params{id};
     my $hooklist = $params{hooklist};

     # do stuff synchronously
     $hooklist->remove( $id ) if $allfinished;

     return;
  }

So now we don't have any significance on the return value any more. We'll need this later - see below.

Now we might consider some more interesting examples. Suppose now we actually care about ordering. And also we allow that any plugin might decide to halt the rest of the list; whatever the event was, it's been handled and there's no need for anyone else to look at it.

So what about ordering? Maybe we give a simple priority number at reg. time?

   $object->register_hook_sync( "event", 10, \&my_first_handler );
   $object->register_hook_async( "event", 20, \&my_second_handler );

This works if we can do it; if we can all agree upfront what we mean by 10, 20, 100, whatever. But this might get hard to coordinate in the necessarily-distrubted environment of whatever it was that needed these pluggable hooklists in the first place.

Stopping the list early becomes easy enough though; either synchronously or async:

  sub my_sync_handler
  {
     my %params = @_;

     .....

     return $handled ? 1 : 0;
  }

  sub my_async_handler
  {
     my %params = @_;

     .....

     later( sub {
        # Choose which continuation
        $params{ $handled ? "last" : "next" }->();
     } );
  }

The result of the call can then say what happened:

  my $handled = $hooklist->fire( @args );
  $hooklist->fire_async( args => \@args, on_done => sub {
     my ( $handled ) = @_;
  } );

Of course, now we have the simplest of hooklist aggregations - the "repeat while false" one. What if we wanted something else? Suppose we wanted to sum some quantity?

  my $hooklist = Hooklist->new(
     aggregate => sub { $a + $b }, # accumulate in $a using the per-hook value $b
  );

  $hooklist->register_sync( \&my_handler );
  $hooklist->register_async( \&my_async_handler );

  $hooklist->fire_async( 
     initial => 0,

     on_done => sub {
        my ( $total ) = @_;
        print "Total: $total\n";
     }
  );

  sub my_handler
  {
     return 20;
  }

  sub my_async_handler
  {
     my %params = @_;

     later( sub {
        $params{next}->( $count );
     } );
  }

If we now give the aggregation function the ability to control the loop, we find that the next/last behaviour above falls out as a natural consequence of how it works:

  my $hooklist = Hooklist->new(
     aggregate => sub { last HOOK if $b },
  );

One Big Question still remains. Ordering. Do we just take priority numbers, or can we find something better?

Perhaps anyone who cares about ordering has a name, and others can ask to be somewhere before/after those named points?

  $hooklist->register_sync(
     name    => "mangle",
     handler => \&my_mangle_function,
  );

  $hooklist->register_sync(
     name    => "eat",
     handler => \&my_eat_function,
     after   => [ "mangle" ],
  );

Then sometime later, some wants to look at this event after it's been mangled but before it's been eaten:

  $hooklist->register_sync(
     name    => "observe",
     handler => \&my_observe_function,
     after   => [ "mangle" ],
     before  => [ "eat" ],
  );

This has the ability to throw an exception if the ordering conditions cannot be met.

This solution has now changed the problem from one of managing absolute numbers, into one of managing names. Hopefully less chance of collisions and mistaken behaviour, but it's more complex to implement and handle. Also harder for the "end user" to picture the behaviour. But maybe it's worth it?

Another interesting question - if two async. handlers are installed at the same priority, would it be best for us to take advantage of that, and run them both in parallel? If we do, we can maximise IO performance of whatever they do, but we run the interesting risk that one of them asks to stop; we can't stop the other one. But maybe that's "hard luck", and just what you get from taking the same number twice..?

-- 
Paul "LeoNerd" Evans

leonerd@leonerd.org.uk
ICQ# 4135350       |  Registered Linux# 179460
http://www.leonerd.org.uk/

Thread Next


nntp.perl.org: Perl Programming lists via nntp and http.
Comments to Ask Bjørn Hansen at ask@perl.org | Group listing | About