PECL::event

Yesterday I added the product of my weekend-of-hackery to PECL: the event extension.

PECL::event implements an event based scheduling engine that will execute a callback when a particular event is triggered. The various different triggers allowed are:

  • EV_READ - a stream becomes ready to read
  • EV_WRITE - a stream becomes ready to write
  • EV_EXCEP - a stream has out-of-band data available to read
  • EV_TIMEOUT - a certain amount of time has elapsed
  • EV_SIGNAL - a signal was raised

As you might have already guessed, PECL::event is probably most useful for longer-lived scripts that need to perform more than network "operation" without either taking too long (why do one after the other if you can do both at the same time?) or without one blocking the other and making it time out.

I've previously given a talk (or was it a magazine article?) on multiplexing with streams in PHP; this extension takes things a step further by taking the complex scheduling logic out of your script. As a bonus, it can also take advantage of the more scalable IO scheduling interfaces (epoll(4), kqueue, /dev/poll, poll(2) and select(2)) available on various different operating systems. Scalable is one of those phrases that can easily be misinterpreted, so in this context more scalable means lower overhead per file descriptor , which should translate to faster execution time in your script.

In practice, you probably won't notice much difference between the different engines in PECL::event, but you should notice the difference between a userspace implementation using stream_select() and PECL::event.

How do I use it?

<?php
  # our callback
  function readable($stream, $mask, $arg) {
     if ($mask & EV_READ) {
       echo "$arg is readable:\\n";
       echo fread($stream, 8192);
       fwrite($stream, "QUIT\\r\\n");
       fclose($stream);
     }
  }
  # detect and activate the best available scheduling engine  
  event_init();
  # create a new event
  $e = event_new(
       # associate it with an async connection to a gmail MX
       stream_socket_client("tcp://gsmtp171.google.com:25",
          $errno, $errstr, 0,
          STREAM_CLIENT_CONNECT|STREAM_CLIENT_ASYNC_CONNECT),
       # ask the engine to tell us when it is ready to read
       EV_READ,
       # tell it to call the 'readable' function when triggered
       'readable',
       # and pass in the following string as an argument
       'gmail mx');
  # put the event into the scheduling engine   
  event_schedule($e);
  # similarly, create another event, this time connecting
  # to the PHP MX 
  $e = event_new(
       stream_socket_client("tcp://osu1.php.net:25",
          $errno, $errstr, 0,
          STREAM_CLIENT_CONNECT|STREAM_CLIENT_ASYNC_CONNECT),
       EV_READ,
       'readable',
       'php mx');
  event_schedule($e);
  # now service all registered events
  event_dispatch();
  # we get here when both events have been run
?>

If you run this script, you should see it output something like this:

  gmail mx is readable:
  220 mx.gmail.com ESMTP 71si267099rna
  php mx is readable:
  220-PHP  is a widely-used general-purpose scripting language
  220-that is especially suited for Web development and can be
  220-embedded into HTML. If you are new to PHP and want to get
  220-some idea of how it works, try the introductory tutorial.
  220-After that, check out the online manual, and the example
  220-archive sites and some of the other resources available in
  220-the links section.
  220-
  220-Do not send UBE to this server.
  220-
  220 osu1.php.net ESMTP Ecelerity HEAD (r3928) Mon, 13 Dec 2004 17:59:20 -0800

You'll see the osu1 banner a couple of seconds after running the script, because that server deliberately pauses.

What's the point?

The script above connects to two SMTP servers, reads their banners and then gracefully closes the connection. So what? Big deal. I can write a much shorter script that does the same thing using just a couple of lines for each host:

<?php
   $fp = fsockopen('gsmtp171.google.com', 25);
   echo fgets($fp);
   fwrite($fp, "QUIT\\r\\n");
   fclose($fp);
   $fp = fsockopen('osu1.php.net', 25);
   echo fgets($fp);
   fwrite($fp, "QUIT\\r\\n");
   fclose($fp);
?>

That may be the case, but with the PECL::event approach, you're not blocking on each connection (remember, osu1 makes you wait a couple of seconds) and you're not blocking on each read. In fact, you could happily add 10s or perhaps even 100s of machines to monitor and have them all happen concurrently. Neat stuff, huh?

That's all I'm going to write for now, but before I go I'll leave you with a couple of interesting snippets if you're interested in playing with the extension.

  • Events are automatically descheduled after they have run once. To get them to run again, you need to either call event_schedule() again or set the EV_PERSIST flag in your event mask when you call event_new(). You can manually event_deschedule() a persistent event to take it out of the scheduling engine (effectively pausing the event).

  • You can specify a deadline (well, a timeout) for an event when you schedule it; the 2nd and 3rd parameters are seconds and microseconds for the timeout.

  • You can call event_new() with a null stream parameter to create an event that is not associated with a stream. event_schedule() it using a timeout to have that function called after a certain amount of elapsed time.

  • The extension is still beta; docs are pending and bug reports are welcome ;)