The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Future::Selector - manage a collection of pending futures

SYNOPSIS

use Future::AsyncAwait;
use Future::IO;
use Future::Selector;
use IO::Socket::IP;

my $selector = Future::Selector->new;

my $listensock = IO::Socket::IP->new(
   LocalHost => "::1",
   LocalPort => "8191",
   Listen => 1,
);

$selector->add(
   data => "listener",
   gen  => sub { Future::IO->accept( $listensock ) },
);

while(1) {
   my @ready = await $selector->select;

   ...
}

DESCRIPTION

Objects in this class maintain a collection of pending Future instances, and manage the lifecycle of waiting for their eventual completion. This provides a central structure for writing asynchronous event-driven programs using Future and Future::IO-based logic.

When writing an asynchronous Future-based client, often the program can be structured similar to a straight-line synchronous program, where at any point the client is just waiting on sending or receiving one particular message or data-flow. It therefore suffices to use a simple call/response structure, perhaps written using the async and await keywords provided by Future::AsyncAwait.

In contrast, a server program often has many things happening at once. It will be handling multiple clients simultaneously, as well as waiting for new client connections and any other internal logic it requires to provide data to those clients. There is not just one obvious pending future at any one time; there could be several that all need to be monitored for success or failure.

A Future::Selector instance helps this situation, by storing an entire set of pending futures that represent individual sub-divisions of the work of the program (or a part of it). As each completes, the selector instance informs the containing code so it can continue to perform the work required to handle that part, perhaps resulting in more future instances for the selector to manage.

Program Structure

As per the SYNOPSIS example, a typical server-style program would probably be structured around a while(1){} loop that repeatedly awaits on the next select future from the selector instance, looking for the next thing to do. The data values stored with each future and returned by the select method can be used to help direct the program into working out what is going on. For example, string names or object instances could help identify different kinds of next step.

use v5.36;

...

$selector->add(
   data => "listener",
   gen  => sub { Future::IO->accept( $listensock ) },
);

while(1) {
   foreach my ( $data, $f ) ( await $selector->select ) {
      if( $data eq "listener" ) {
         # a new client has been accept()ed. should now set up handling
         # for it in some manner.

         my $sock = await $f;
         my $clientconn = ClientConnection->new( fh => $sock );

         $selector->add( data => $clientconn, f => $clientconn->run );
      }
      elsif( $data isa ClientConnection ) {
         # an existing connection's runloop has terminated. should now
         # handle that in whatever way is appropriate
         ...
      }
      ...
   }
}

Alternatively, if each stored future instance already performed all of the work required to handle it before it yields success, there may be nothing for the toplevel application loop to do other than repeatedly wait for things to happen.

$selector->add(
   data => undef, # ignored
   gen  => async sub {
      my $sock = await Future::IO->accept( $listensock );
      my $clientconn = ClientConnection->new( fh => $sock );

      $selector->add( data => undef, f => $clientconn->run );
   }
);

await $selector->select while 1;

Failure propagation by the select method here ensures any errors encountered by individual component futures are still passed upwards through the program structure, ultimately ending at the toplevel if nothing else catches it first.

Comparison With select(2), epoll, etc..

In some ways, the operation of this class is similar to system calls like select(2) and poll(2). However, there are several key differences:

  • Future::Selector stores high-level futures, rather than operating directly on system-level filehandles. As such, it can wait for application-level events and workflow when those things are represented by futures.

  • The main waiting call, "select", is a method that returns a future. This could be returned from some module or component of a program, to be awaited on by another outer Future::Selector instance. The application is not limited to exactly one as would be the case for blocking system calls, but can instead create a hierarchical structure out of as many instances as are required.

  • Once added, a given future remains a member of a Future::Selector instance until it eventually completes; which may require many calls to the select method (or indeed, it may never complete during the lifetime of the program, for tasks that should keep pending throughout). In this way, the object is more comparable to persistent system-level schedulers like Linux's epoll or BSD's kqueue mechanisms, than the one-shot nature of select(2) or poll(2) themselves.

METHODS

add

$selector->add( data => $data, f => $f );

Adds a new future to the collection.

After the future becomes ready, the currently-pending select future (or the next one to be created) will complete. It will yield the given data and future instance if this future succeeded, or fail with the same failure if this future failed. At that point it will be removed from the stored collection.

$selector->add( data => $data, gen => $gen );

   $f = $gen->();

Adds a new generator of futures to the collection.

The generator is a code reference which is used to generate a future, which is then added to the collection similar to the above case. Each time the future becomes ready, the generator is called again to create another future to continue watching. This continues until the generator returns undef.

select

( $data1, $f1, $data2, $f2, ... ) = await $selector->select();

Returns a future that will become ready when at least one of the stored futures is ready. It will yield an even-sized list of pairs, giving the associated data and original (now-completed) futures that were stored.

If you are intending to run the loop indefinitely, be careful not to write code such as

1 while await $selector->select;

because in scalar context, the awaited future will yield its first value, which will be the data associated with the first completed future. If that data value was false (such as undef) then the loop will stop running at that point. Generally in these sorts of situations you want to use "run" or "run_until_ready".

run

await $selector->run();

Since version 0.02.

Returns a future that represents repeatedly calling the "select" method indefinitely. This will not return, except that if any of the contained futures fails then this will fail the same way.

This is most typically used at the toplevel of a server-type program, one where there is no normal exit condition and the program is expected to remain running unless some fatal error happens.

run_until_ready

@result = await $selector->run_until_ready( $f );

Since version 0.02.

Returns a future that represents repeatedly calling the "select" method until the given future instance is ready. When it becomes ready (either by success or failure) the returned future will yield the same result.

The given future will be added to the selector by calling this method; you should not call "add" on it yourself first.

This is typically used in client or hybrid code, or as a nested component of a server program, which needs to wait on a result while also performing other background tasks.

Remember that since this method itself returns a future, it could easily serve as the input to another outer-level selector instance.

TODO

  • Convenience ->add_f / ->add_gen

  • Configurable behaviour on component future failure

AUTHOR

Paul Evans <leonerd@leonerd.org.uk>