Skip to content

Rearm_Before_Original_Expiry_Extends_The_Deadline schlägt auf net48 fehl — Last erklärt es nicht #130

Description

@dborgards

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: 658Job-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:

  1. Arm(600 ms)
  2. await Task.Delay(50 ms)
  3. Rearm(2000 ms) — die Assertion darauf ist durchgelaufen, die Deadline war also noch Pending, der ursprüngliche Timer hatte nicht gefeuert.
  4. 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

  1. Auf net48 reproduzieren, am besten in Schleife und unter Last. Ohne Reproduktion ist jede Änderung hier geraten.
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: reliabilityCanKit.Pro.Reliability — deadlines, bus statearea: testsTest suite, coverage, API approvalstype: bugSomething behaves differently than documented

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions