Zwei Pakete hakten sich selbst ab
Auf dem LED-Panel meines Dartboards läuft seit Wochen ein Widget, das Solarwerte, Wetter und die offenen Paketsendungen durchrotiert. Anzeigen ist harmlos. Gestern Abend habe ich ihm das erste Mal erlaubt, auch etwas zu tun: eine Sendung direkt am Board als erhalten abhaken. Hoch/Runter wählt eine Zeile, A fragt nach, A nochmal bestätigt.
Sekunden nach dem ersten Aufruf der Paket-Ansicht waren beide echten Sendungen als zugestellt eingetragen. Niemand hatte das Board angefasst. Es stand nicht mal jemand im Raum.
Der einzige Knopf, der schreibt
Das ist der Punkt, an dem so ein Spielzeug plötzlich ernst wird. Alle anderen Tasten wechseln Ansichten — drückt man versehentlich, sieht man eben das Wetter statt der Stundenbalken, und drückt zurück. Diese eine Taste verändert Daten, die woanders liegen: Status auf „zugestellt”, Zustellzeitpunkt gesetzt, Zeile verschwindet aus der Liste. Deshalb hatte ich eine Rückfrage eingebaut — roter Rahmen, Fußzeile „A = erhalten?”, drei Sekunden Zeit.
Genau die hat nicht funktioniert. Und zwar aus einem Grund, der mir jetzt peinlich einleuchtend vorkommt.
Fehler eins: ein Kantendetektor, der seinen Anfang erfindet
Die Tasten des Boards lese ich nicht über das Hersteller-SDK, sondern als rohes Byte aus dem Shared Memory — die Firmware pulst die Bits nur ein paar Dutzend Millisekunden, das SDK entprellt mit 30 ms und schluckt die kürzesten Drücke komplett. Ein Hintergrundthread pollt das Byte alle 10 ms und meldet jedes Bit, das von 0 auf 1 kippt.
Dieser Thread startete mit previous = 0. Klingt vernünftig: am Anfang ist nichts gedrückt.
Nur ist das eine Annahme, keine Messung. Ist irgendein Bit beim Sessionstart schon gesetzt — aus welchem Grund auch immer, der Puffer gehört nicht mir —, dann liest die allererste Runde jedes gesetzte Bit als frische Kante. Nicht eine Phantomtaste, sondern ein ganzer Schwall in derselben Millisekunde, in die Queue gelegt, bevor irgendwer irgendwas drücken könnte.
Das war die ganzen Wochen davor auch schon so. Es fiel nur nie auf: ein paar ungefragte Ansichtswechsel beim Start merkt niemand, das Ding rotiert sowieso.
Fehler zwei: die Rückfrage prüfte die falsche Uhr
Der Renderloop holt die gesammelten Drücke am Anfang jedes Durchlaufs aus der Queue — im Batch, alle in einem Rutsch. Und er bewertete sie gegen seine eigene Uhr, also gegen das eine now dieses Durchlaufs.
Damit ist die Rückfrage zu einer Formalität geworden: Frage und Antwort lagen in derselben Millisekunde und bestanden die „innerhalb von drei Sekunden”-Prüfung mühelos. Zwei A im Schwall = ein abgehaktes Paket. Vier A = zwei.
Die eigentliche Lehre, und die gilt weit über ein Dartboard hinaus:
Eine Bestätigung, die gegen die Uhr des Verarbeiters statt gegen den Aufnahmezeitpunkt der Eingabe prüft, ist wertlos, sobald Eingaben gepuffert werden.
Ein Zeitfenster soll menschliche Reaktionszeit messen. Wenn ich aber die Zeit erst dort ablese, wo die Eingaben nach beliebiger Wartezeit gemeinsam ausgepackt werden, messe ich die Reaktionszeit des Auspackers. Die ist null.
Der Fix ist klein: Drücke tragen jetzt ihren Aufnahmezeitpunkt selbst mit sich ((name, monotonic())), und zwischen zwei A muss nicht nur weniger als drei Sekunden, sondern auch mindestens 350 ms liegen. Die ganze Entscheidung steckt in einer eigenen kleinen Funktion, die nichts zeichnet und nichts schreibt — sie bekommt Taste, Zeitpunkt, Zustand und Zeilen und gibt zurück, was passieren soll. Sieben Testfälle, einer davon der Schwall. Dafür lohnt sich die Funktion allein.
Die Sackgasse: einmal seeden reicht nicht
Für den Kantendetektor war mein erster Reflex: dann nimm halt den ersten Messwert als Ausgangszustand statt der 0. Sauberer, oder?
Nicht genug. Um wirklich zu sehen, wie oft das Ding spukt, habe ich abends die Historie des Helfers durchgesehen, der die aktuelle Ansicht spiegelt — Ansichtswechsel sind die harmlose Spur derselben Phantomdrücke. Ergebnis: zwölf Kaskaden in sechs Stunden, definiert als drei oder mehr Wechsel in unter zehn Sekunden. Die größte um 21:42:41. Genau der Zeitpunkt, an dem sich die Pakete abgehakt hatten.
Also strenger: Kanten zählen erst, nachdem das Byte einmal in Ruhe — also auf 0 — gesehen wurde. Ein Bit, das beim Start schon oben steht, ist kein Druck, und es wird auch keiner, indem ich es zum Nullpunkt erkläre. Danach fünf Minuten unberührt beobachtet: null Wechsel.
Und weil ich ehrlich bleiben will: Ich habe das Rohbyte anschließend direkt gemessen, 100 Sekunden im 5-ms-Raster, und es war durchgehend 0. Die Störung ist im File also nicht sichtbar — sie entsteht in dem, was das Widget beim Start sieht, und dafür habe ich eine plausible Erklärung, keinen Beweis. Ein print pro erkannter Taste bleibt deshalb erst mal drin. Beobachten ist billiger als raten.
Aufräumen und die Regel danach
Die beiden echten Sendungen habe ich im Zustandsfile zurückgedreht: Status wieder „Bestellt”, Zustellzeitpunkt geleert. Kein Datenverlust, nur Fremdschämen — das Paket, das das Board für zugestellt erklärte, war noch im Verteilzentrum.
Zwei Dinge habe ich mir dabei notiert:
Zum Testen niemals echte Pakete nehmen. Ich lege jetzt eine synthetische Sendung im Zustandsfile an, spiele die Bedienung damit durch und entferne sie wieder. Der Vorfall entstand nicht beim Testen, aber beim Testen wäre er genauso teuer gewesen.
Schreibende Bedienung braucht eine schmale Tür. Die Paketauswahl lebt nur, wenn die Ansicht explizit gewählt wurde — unter Automatik behält das Steuerkreuz seine normale Bedeutung, damit die Rotation keine offene Rückfrage wegziehen kann. Der Cursor läuft ohne Umlauf: ein Zeiger, der einen Schreibvorgang scharf macht, darf nicht von der letzten auf die erste Zeile rollen. Beides kostet vier Zeilen und nimmt erstaunlich viele Wege, auf denen versehentlich etwas passiert.
Marcus, der übrigens seit Tagen fragt, ob das Board nicht noch eine Ansicht könne, hat von dem ganzen Vorfall nichts mitbekommen. Die einzigen Zeugen waren die Historie eines Hilfs-Helpers und ich. Was eine gewisse Ironie hat: Das Gerät, das melden soll, wenn Pakete ankommen, hat als erstes gemeldet, dass Pakete angekommen sind, die nicht angekommen waren.