25 Tage später …

the next big thing  –  4 Kommentare

Vor etwas über einem Monat wurde ein merkwürdiger Fehler in Node.js entdeckt: Zeitbezogene Funktionen wie setTimeout und setInterval stellen nach 25 Tagen den Betrieb ein. Glücklicherweise wurde das Problem zwischenzeitlich behoben. Doch was war die Ursache?

Das Issue liest sich kurios und klingt zunächst eher nach einem Fehler des meldenden Entwicklers: Ihm sei aufgefallen, dass die Funktion setInterval nach ungefähr einem Monat aufhöre zu funktionieren. Die Anwendung an sich liefe aber weiter und antworte auch nach wie vor auf Anfragen. Nur der Timer wolle eben nicht mehr.

Nähere Untersuchungen haben das Problem dann eingegrenzt: Die Funktionen setTimeout und setInterval stellen jeweils nach 25 Tagen den Betrieb ein – allerdings nur in Node.js-Versionen der 10.x-Serie. Zum Provozieren des Fehlers genügt ein einfaches Skript:

setInterval(() => {
console.log(Date.now());
}, 1000);

Die weitere Fehlersuche hat zu der Datei env.cc geführt, die eine Funktion namens getNow mit folgendem Code enthielt. Die problematische Stelle ist dabei fett hervorgehoben:

Local<Value> Environment::GetNow() {
uv_update_time(event_loop());
uint64_t now = uv_now(event_loop());
CHECK_GE(now, timer_base());
now -= timer_base();
if (now <= 0xffffffff)
return Integer::New(isolate(), static_cast<uint32_t>(now));
else
return Number::New(isolate(), static_cast<double>(now));
}

Das Problem an der Stelle ist, dass die Variable now, wie zuvor deklariert, vom Typ uint64_t ist. Es handelt sich also um eine vorzeichenlose Ganzzahl. Durch die Konvertierung per Integer::New wird sie aber – fälschlicherweise – in eine vorzeichenbehaftete Ganzzahl konvertiert, was ihren Wert natürlich ungewollt verändert.

Dementsprechend wird die Zeile im zugehörigen Pull-Request durch die folgende, korrigierte Fassung ersetzt:

if (now <= 0xffffffff)
return Integer::NewFromUnsigned(isolate(), static_cast<uint32_t>(now));

Die korrigierte Version ist in allen Node.js-Versionen ab 10.9.0 enthalten, es empfiehlt sich also, auf eine neue Version zu aktualisieren (zumindest dann, wenn man Node.js 10.x einsetzt und langlaufende Prozesse betreibt, die auf zeitgesteuerte Aktionen ausführen).

Bleibt die Frage, wie man einen solchen Fehler nachstellen kann: Schließlich müsste man nach dem Anpassen des Codes wieder 25 Tage warten, um festzustellen, ob der Fehler behoben ist. Abhilfe schafft hier eine kleine Bibliothek namens libfaketime, mit der man einzelnen Prozessen eine geänderte Systemzeit vorgeben kann – oder auch die Zeit einfach schneller ablaufen lassen kann.

Zu dem Zweck stellt die Bibliothek ein Kommandozeilenwerkzeug namens faketime zur Verfügung, mit dem man eine Anwendung auf einfachem Weg stark beschleunigt ausführen kann:

$ faketime -f "+10 x1000" node app.js

tl;dr: Node.js enthält in der 10.x-Serie bis zur Version 10.9.0 den Fehler, dass die Funktionen setTimeout und setInterval nach 25 Tagen ihren Betrieb einstellen. Ursache ist ein Konvertierungsfehler. Abhilfe schafft das Aktualisieren auf eine neue Node.js-Version.