Reference in New Issue
Block a user
Delete Branch "worker/deterministic-stamp-race-409-3cb7b6-10"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
fleetd #409 — deterministic test for the #399 completion-stamp ordering race
What this adds
A new test,
aTicketOrderedOnDoneInsteadOfTheCompletionStampSurvivesAnEvictionItMustNotSurvive,in
MessageServiceTest.java. It pins this invariant:No production code changed —
pruneTerminalTickets'sexpiredexpression andTICKET_TTL_NANOSare untouched. The existing
awaitCompletionStampedhelper and both its call sites are unchanged.The seam
MessageService's injectedLongSupplier nowNanos(constructor atMessageService.java:342) hasexactly three call sites:
Task's constructor (createdNanos),Task'swhenCompletehook(
completedNanos), andpruneTerminalTickets's cutoff. I confirmed this by greppingnowNanosinMessageService.javabefore designing anything — no new production seam was needed.Why the real race can't be hit by chance in one run
Per my memory of fleetd #399's own investigation, that race needed roughly 2x-core background load
on the host to manifest at all — a plain run is not enough to expose it deterministically. So this
test does not try to win that race by chance. It widens it on purpose: the injected clock is a
"gated"
LongSupplierthat sleeps 300ms the one time it is armed to do so (armed right beforerendezvous.resolve(...)triggers the async completion, and consumed by whichever call happensnext — which, given the three call sites above and nothing else running nowNanos in between, is
unambiguously the completion hook's own stamp for the ticket under test). That gives the test
thread a guaranteed-wide window (300ms, vs. microsecond-scale test-thread work) to advance the
clock and run a sweep before the hook's read returns, deterministically, on any host.
The two runs (acceptance criterion 1 and 2)
With the barrier (
awaitCompletionStamped) present — current/correct ordering:Ran 3x in a row — 3/3 green, first try every time.
With the barrier call removed (reproducing the pre-#399 ordering):
Ran 3x in a row — 3/3 red, first try every time, same failure every time.
Which way it fails, and how I determined it (acceptance criterion 4)
Without the barrier, the test fails as a genuine RED assertion failure, not a false pass:
assertNull(service.poll(ticket))fails becausepollstill returns the ticket's DONE view — thesweep did not evict it. I determined this by actually removing the
awaitCompletionStampedcall,running the test (see failure text above), and reading the resulting
TaskViewin the failuremessage: it is the exact object
pollwould return for a ticket that was never pruned, not anexception or a timeout from my own synchronization code. This is the intended mechanism (the hook
stamps the clock's already-advanced value, so
cutoff = advanced - TTLcan never exceed it) andnot some other defect in the test's wiring.
I want to flag one nuance for review: the ticket's own framing calls the pre-#399 failure mode "a
FALSE PASS, not a red test." I read that as describing the general danger of this race (a test
shaped to assert survival, like the sibling
aTaskRunningLongerThanTheTtlStillKeepsItsReport,would falsely stay green under this exact race, per that test's own #399 comment — "happens to
survive a late stamp today"), not a requirement that my test's own failure mode be a false pass.
Acceptance criterion 1 explicitly requires the new test to fail without the barrier, which is
what I built and verified. If I've misread that framing, happy to adjust.
Build
MessageServiceTestalone: 83 tests (was 82), 0 failures/errors.Files changed
fleetd/src/test/java/dev/ltms/fleet/msg/MessageServiceTest.java(+74 lines, test-only)Also found, not fixed (per ticket instructions — report only)
reading a value a dependent action of that same completion writes) was found in
MessageServiceTest.javabeyond the twoawaitCompletionStampedcall sites already fixed by#399 — I did not do a repo-wide sweep beyond this file, since that's outside this ticket's scope.