tidy-event: the merger's gen counter does not span a Flush - #289
Merged
Conversation
Merger.gen is documented as the answer to "a timer that has begun firing cannot be stopped". It counts per ENTRY, and a fresh entry starts at zero — so it answers that question for a settler reset and not for the same race across Flush. The sequence: a key's timer begins firing, so its stop() returns false; Flush detaches and emits that entry and clears the map; the same key is Added again, producing a new entry at gen 0; the stranded callback then matches the new entry by generation and fires it immediately rather than a window later. A coalescer entry is never reset, so its timer always holds gen 0, which is what makes the collision reachable rather than theoretical. Reproduced deterministically through the injectable timerFactory before recording. The consequence is mild. One notification arrives up to a window early, and the bus contract already makes that safe: consumers re-read authoritative state rather than treating an event as a log. What is worth recording is that the mechanism which would prevent it is the one already described two lines above, applied one level too narrowly — a monotonic sequence on the Merger would give every armed timer a generation nothing else can hold. That is a change of behaviour with a regression test to write, so it is recorded beside the invariant it bends rather than made on a tidy night. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nightly tidy pass over
internal/event(2026-08-10 07:18 round). Records abehaviour defect; the fix follows on its own branch.
Merger.genis documented as the answer to "a timer that has begun firingcannot be stopped". It counts per entry, and a fresh entry starts at zero,
so it answers that question for a settler reset and not for the same race
across
Flush:stop()returns false;Flushdetaches and emits that entry and clears the map;Added again — a new entry, atgen0;immediately instead of a window later.
A coalescer entry is never reset, so its timer always holds
gen0, which iswhat makes the collision reachable rather than theoretical. Reproduced
deterministically through the injectable
timerFactorybefore recording.The consequence is mild and the report says so: one notification arrives up
to a window early, and the bus contract already makes that safe — consumers
re-read authoritative state rather than treating an event as a log. What is
worth recording is that the mechanism which would prevent it is the one
described two lines above in the same document, applied one level too narrowly.
A monotonic sequence on the
Mergergives every armed timer a generationnothing else can hold, and subsumes the settler case.
docs/modules/config.mdbeside thegeninvariant it bends.
bus.go'sremove-then-close ordering against
Publish's read lock is stated andcorrect, and
firealready detaches under the lock and builds outside it.Short lane: docs-only, so
go test ./test/buildrules/is the localverdict. Green.