Libevent Woes in PHP
Seit ein paar Wochen arbeite ich zum Spaß an einem STOMP Client, der libevent benutzt um dann Listener einer Queue per Symfony Event Dispatcher zu benachrichtigen sobald neue Messages verfügbar sind. Dazu benutze ich die PHP bindings für libevent. Wer libevent nicht kennt: das ist eine Bibliothek in C geschrieben, die es einfacher macht mit Streams (z. B. Sockets) zu arbeiten, in dem man sich auf bestimmte Events bindet die auf dem Stream passieren (z. B. read/write/timeout) und dann von libevent das gewünschte callback aufgerufen wird. Intern beutzt libevent die für die Umgebung performanteste Polling Implementierung um nachzuschauen ob neue Daten verfügbar sind im Stream.
Grunsätzlich funktioniert die Extension super, ich musste nur (damals) die neueste Variante aus dem pecl svn installieren (kann man sich holen mit svn checkout http://svn.php.net/repository/pecl/libevent/trunk libevent-trunk), ansonsten wollte die Extension nicht mit PHP 5.4. kompilieren. Ist aber in der neueren Version gefixed.
Da der STOMP Client noch nicht fertig ist, gibts dazu später einen extra Artikel, aber ein paar Dinge sind mir beim arbeiten mit libevent aufgefallen, die ich gerne festhalten möchte.
Event loops are hard to kill
In diesem Beispiel pollen wir STDIN nach neuen Daten, nur um dann im read callback eine Exception zu werfen:
$base = event_base_new();
$eb = event_buffer_new(
STDIN,
function ($buf, $arg) use(&$base) {
print 'read callback' . PHP_EOL;
throw new Exception();
},
NULL,
function ($buf, $what, $arg) {
print 'error callback' . PHP_EOL;
},
$base
);
event_buffer_base_set($eb, $base);
event_buffer_enable($eb, EV_READ);
event_base_loop($base);
Da ich event loops ja aus node.js schon kenne, hätte ich gedacht, die Exception würde die event loop beenden und das script somit auch und den Stacktrace auf der Konsole anzeigen. Falsch. Es passiert nämlich gar nix:
$ php loop.php test read callback triggered (hängt für immer)
Die Exception wird nirgends ausgegeben, gar nix. Möglicherweise ändert sich das in künfigen Versionen der PHP Extension, aber bis dahin hat mir nur geholfen die event loop explizit zu beenden und danach die Exception zu werfen (hier der geänderte Aufruf für einen neuen buffered event):
$eb = event_buffer_new(
STDIN,
function ($buf, $arg) use(&$base, &$eb) {
print 'read callback' . PHP_EOL;
// loop beenden, cleanup macht die extension zum Glück für uns
event_base_loopbreak($base);
throw new Exception();
},
NULL,
function ($buf, $what, $arg) {
print 'error callback' . PHP_EOL;
},
$base
);
Error handling
Die PHP Extension gleicht vom Abstraktionslevel der C Implementierung von libevent ziemlich genau, nur das cleanup/freigeben von Resourcen bleibt einem erspart. Ansonsten kann man sich genauso wie in C mit Fehlercodes rumplagen:
$some_remote_socket = fsockopen(...);
$base = event_base_new();
$eb = event_buffer_new(
$some_remote_socket,
function ($buf, $arg) {
// ...
},
NULL,
function ($buf, $what, $arg) {
// $what enthält den Fehlercode
if ($what & EVBUFFER_EOF) print 'unerwartetes EOF' . PHP_EOL;
else if ($what & EVBUFFER_ERROR) print 'allgemeiner fehler (sehr hilfreich, ja)' . PHP_EOL;
else if ($what & EVBUFFER_TIMEOUT) print 'timeout' . PHP_EOL;
},
$base
);
event_buffer_timeout_set($eb, 10, 10);
event_buffer_base_set($eb, $base);
event_buffer_enable($eb, EV_READ);
event_base_loop($base);
Exceptions kann die Extension ja aber auch schlecht werfen, siehe vorheriger Punkt.
Event loop unterbrechen
In meinem STOMP Client muss ich ab und an die event loop unterbrechen, was zwischendrin machen und dann die event loop wieder starten. Der Zeitraum zwischen unterbrechen und neustarten ist SEHR kurz, aber: wenn nach der Unterbrechung und VOR dem erneuten Starten der event loop Daten in den Socket geschrieben wurden vom Server, dann habe ich diese Daten verpasst und mich gewundert wieso. Folgendes Pattern hat sich dabei bewährt:
- Event loop stoppen mit event_base_loopbreak($base)
- Schon mal neues event base erzeugen mit $base = event_base_new()
- Read event erzeugen, event base zuordnen, event enablen event_buffer_new() usw.
- Event loop NOCH NICHT wieder starten
- Das tun, was zwischendrin getan werden muss in der app
- Jetzt event loop wieder starten mit event_base_loop($base)
- Sollten von einem sehr schnellen Server Daten zwischendrin in den Socket geschrieben worden sein, wird der read event dispatched von libevent
Schön ist was anderes, aber hat geholfen einen fiesen Bug zu fixen.
Unit Testing
Unit Testing ist generell ein schwieriges Unterfangen wenn PHP Funktionen im Spiel sind, dazu kommt noch die Abhängigkeit von externe Resourcen wie Sockets. Ich hab das Problem größtenteils umgangen in dem ich auf Unit Tests für die Teile, die libevent Funktionen benutzen, verzichtet habe und setze lieber eine Integration Test Suite auf. In meinem Fall des STOMP Clients verwende ich dazu einem (Dummy) STOMP Server in node.js geschrieben. Ich teste lieber den Output den ich vom Server bekomme anstatt zu prüfen ob eine Methode event_base_loop aufgerufen hat.
Aber sonst…
Insgesamt bin ich mit libevent mehr als zufrieden und bin schon sehr gespannt auf den STOMP client, sodann ich dessen TODO Liste endlich mal runtergearbeitet habe…