Herausgelöst aus #92 auf Wunsch des Maintainers. #92 sammelt Tests, die auf ausgelasteten Runnern an der Uhr scheitern; dieser Fall gehört nicht dazu, und der Unterschied ist der Grund für das eigene Ticket.
Beobachtung
CanKit.Pro.Tests.TestCases.DeadlineTests.Rearm_Before_Original_Expiry_Extends_The_Deadline, DeadlineTests.cs:100:
Did not expect object to be equal to System.Threading.Tasks.Task`1[System.Boolean]
{Status=RanToCompletion} because the original timeout must have been superseded by Rearm.
Windows-Leg, net48. Failed: 1, Passed: 658 — Job-Log. Der net10.0-Lauf desselben Jobs war grün, ebenso ubuntu-latest und macos-latest auf demselben Commit.
Aufgetreten auf 47b57df (PR #129) — einem Branch, dessen Diff gegen main null .cs-Dateien enthält.
Warum „ausgelasteter Runner" hier nicht trägt
Gemessen, lokal, 4 Kerne:
| Bedingung |
Läufe |
Ergebnis |
| net10.0, unbelastet |
12 |
12 bestanden |
| net10.0, 3× CPU-Überlast (12 Endlosschleifen) |
8 |
8 bestanden |
20 von 20 unter Bedingungen, die härter sind als ein geteilter CI-Runner. net48 ließ sich hier nicht gegenprüfen (kein Mono im Container) — die Messung gilt ausdrücklich nur für net10.0.
Dazu passt die Arithmetik des Tests nicht zur Überschwing-Erklärung. Der Ablauf:
Arm(600 ms)
await Task.Delay(50 ms)
Rearm(2000 ms) — die Assertion darauf ist durchgelaufen, die Deadline war also noch Pending, der ursprüngliche Timer hatte nicht gefeuert.
- Prüffenster 850 ms: der Callback darf nicht feuern. Er feuerte.
Der neu gesetzte Timer läge bei 2000 ms nach Schritt 3. Ihn in dieses Fenster zu bekommen bräuchte über eine Sekunde Fehler. Die naheliegendere Lesart ist die, gegen die der Test geschrieben wurde: der ersetzte Timer feuert trotzdem, die Generationsprüfung greift nicht.
Die Fenster sind außerdem bereits einmal geweitet worden — der Kommentar im Test sagt es selbst („deliberately far apart rather than merely different"). Und #92 warnt an genau dieser Stelle: „if the client is wrong, widening a tolerance would have hidden a defect."
Was ich geprüft und ausgeschlossen habe
Damit niemand dieselben Wege zweimal geht:
- Keine bedingte Kompilierung.
grep -rn "NETFRAMEWORK\|NET48\|#if NET" src/CanKit.Pro.Reliability/ src/CanKit.Pro.Actor/ findet nichts. Beide Ziele bauen denselben Quelltext.
- Kein Einheitenfehler in der Zeitrechnung. Naheliegender Verdacht, weil
Stopwatch.Frequency auf .NET Framework und .NET 10 verschieden ist. Trägt aber nicht: ProtocolActor.DueTimestamp rechnet delay.TotalSeconds * _time.Frequency, der Rückweg ticks / _time.Frequency (:151-154, :757). Frequenz-agnostisch, auf beiden Zielen konsistent.
- Die Generationsprüfung liest sich korrekt.
Rearm verwirft den Handle und erhöht die Generation vor dem Neuarmieren (DeadlineScheduler.cs:142-143), ArmLocked fängt den Wert unter demselben Lock ein (:212), und Fire prüft ihn vor dem terminalen CAS (:226-227). Statisch ist daran nichts zu sehen.
Ich habe also keine Ursache gefunden, nur zwei Hypothesen entkräftet. Was bleibt, ist laufzeitspezifisch und braucht eine Reproduktion auf net48 — vermutlich rund um SemaphoreSlim-Wartepräzision oder das Thread-Scheduling der Actor-Schleife.
Vorgeschlagene Reihenfolge
- Auf net48 reproduzieren, am besten in Schleife und unter Last. Ohne Reproduktion ist jede Änderung hier geraten.
- Wenn er reproduzierbar ist: feststellen, welcher Timer feuert — der ersetzte oder der neue. Ein Logeintrag mit der Generation in
Fire beantwortet das in einem Lauf und unterscheidet Produktfehler von Testfehler.
- Erst danach entscheiden, ob die Deadline-Primitive oder der Test zu ändern ist.
Die Toleranz zu weiten ist in keinem dieser Fälle die Antwort: entweder greift der Generationsschutz nicht — dann verdeckt Weiten einen echten Fehler in genau der Primitive, die #92 bei seinem UDS-Fall bereits verdächtigt — oder der Zeitplan geht um über eine Sekunde daneben, und das ist für sich ein Befund.
Bezug zu #92
#92 vermutet beim UDS-Fall, dass „a deadline that has already expired" gegen eine später eingetroffene Antwort verliert, und nennt das „what the L2 deadline primitive was introduced to prevent — an expiry that is guaranteed to be checked". Dieses Ticket trifft dieselbe Primitive an ihrer anderen Flanke: nicht eine Frist, die zu spät geprüft wird, sondern eine ersetzte Frist, die trotzdem feuert. Wer eines von beiden reproduziert, sollte das andere mitprüfen.
Herausgelöst aus #92 auf Wunsch des Maintainers. #92 sammelt Tests, die auf ausgelasteten Runnern an der Uhr scheitern; dieser Fall gehört nicht dazu, und der Unterschied ist der Grund für das eigene Ticket.
Beobachtung
CanKit.Pro.Tests.TestCases.DeadlineTests.Rearm_Before_Original_Expiry_Extends_The_Deadline,DeadlineTests.cs:100:Windows-Leg, net48.
Failed: 1, Passed: 658— Job-Log. Der net10.0-Lauf desselben Jobs war grün, ebensoubuntu-latestundmacos-latestauf demselben Commit.Aufgetreten auf
47b57df(PR #129) — einem Branch, dessen Diff gegenmainnull.cs-Dateien enthält.Warum „ausgelasteter Runner" hier nicht trägt
Gemessen, lokal, 4 Kerne:
20 von 20 unter Bedingungen, die härter sind als ein geteilter CI-Runner. net48 ließ sich hier nicht gegenprüfen (kein Mono im Container) — die Messung gilt ausdrücklich nur für net10.0.
Dazu passt die Arithmetik des Tests nicht zur Überschwing-Erklärung. Der Ablauf:
Arm(600 ms)await Task.Delay(50 ms)Rearm(2000 ms)— die Assertion darauf ist durchgelaufen, die Deadline war also nochPending, der ursprüngliche Timer hatte nicht gefeuert.Der neu gesetzte Timer läge bei 2000 ms nach Schritt 3. Ihn in dieses Fenster zu bekommen bräuchte über eine Sekunde Fehler. Die naheliegendere Lesart ist die, gegen die der Test geschrieben wurde: der ersetzte Timer feuert trotzdem, die Generationsprüfung greift nicht.
Die Fenster sind außerdem bereits einmal geweitet worden — der Kommentar im Test sagt es selbst („deliberately far apart rather than merely different"). Und #92 warnt an genau dieser Stelle: „if the client is wrong, widening a tolerance would have hidden a defect."
Was ich geprüft und ausgeschlossen habe
Damit niemand dieselben Wege zweimal geht:
grep -rn "NETFRAMEWORK\|NET48\|#if NET" src/CanKit.Pro.Reliability/ src/CanKit.Pro.Actor/findet nichts. Beide Ziele bauen denselben Quelltext.Stopwatch.Frequencyauf .NET Framework und .NET 10 verschieden ist. Trägt aber nicht:ProtocolActor.DueTimestamprechnetdelay.TotalSeconds * _time.Frequency, der Rückwegticks / _time.Frequency(:151-154,:757). Frequenz-agnostisch, auf beiden Zielen konsistent.Rearmverwirft den Handle und erhöht die Generation vor dem Neuarmieren (DeadlineScheduler.cs:142-143),ArmLockedfängt den Wert unter demselben Lock ein (:212), undFireprüft ihn vor dem terminalen CAS (:226-227). Statisch ist daran nichts zu sehen.Ich habe also keine Ursache gefunden, nur zwei Hypothesen entkräftet. Was bleibt, ist laufzeitspezifisch und braucht eine Reproduktion auf net48 — vermutlich rund um
SemaphoreSlim-Wartepräzision oder das Thread-Scheduling der Actor-Schleife.Vorgeschlagene Reihenfolge
Firebeantwortet das in einem Lauf und unterscheidet Produktfehler von Testfehler.Die Toleranz zu weiten ist in keinem dieser Fälle die Antwort: entweder greift der Generationsschutz nicht — dann verdeckt Weiten einen echten Fehler in genau der Primitive, die #92 bei seinem UDS-Fall bereits verdächtigt — oder der Zeitplan geht um über eine Sekunde daneben, und das ist für sich ein Befund.
Bezug zu #92
#92 vermutet beim UDS-Fall, dass „a deadline that has already expired" gegen eine später eingetroffene Antwort verliert, und nennt das „what the L2 deadline primitive was introduced to prevent — an expiry that is guaranteed to be checked". Dieses Ticket trifft dieselbe Primitive an ihrer anderen Flanke: nicht eine Frist, die zu spät geprüft wird, sondern eine ersetzte Frist, die trotzdem feuert. Wer eines von beiden reproduziert, sollte das andere mitprüfen.