Skip to content

merger-gen: one generation sequence per merger, not per entry - #290

Merged
dinstein merged 2 commits into
mainfrom
merger-gen
Aug 9, 2026
Merged

merger-gen: one generation sequence per merger, not per entry#290
dinstein merged 2 commits into
mainfrom
merger-gen

Conversation

@dinstein

@dinstein dinstein commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Fixes the defect recorded in docs/modules/config.md by the 2026-08-10 tidy
pass over internal/event.

Merger.gen answered "a timer that has begun firing cannot be stopped" for a
settler reset and not for the same race across Flush. It counted per entry
and a fresh entry started at zero, while a coalescer entry is never reset — so
its stranded callback always held zero too, and the two collided: a key's timer
begins firing, Flush detaches and emits that entry, the same key is Added
again at generation zero, and the stranded callback matches the new entry and
fires it a window early.

The number now comes from a monotonic sequence on the Merger, so a stranded
callback can match nothing but the timer it was armed for. That subsumes
the settler case rather than replacing it — the reset path draws from the same
sequence.

The consequence was mild and the commit says so: one notification arrived
early, which the bus contract already makes safe. It is fixed because the
mechanism that prevents it was already there, applied one level too narrowly.

  • The sequence, plus regression tests for both races, driven by a
    hand-run timer rather than a clock — the interleaving is the subject,
    and a sleep would test the scheduler instead. Verified the new case
    fails on the previous code.
  • docs/modules/config.md: the GAP entry becomes the invariant, keeping
    the account of the collision because it is what explains why the counter
    is not on the entry.

One note on the test, since it cost a cycle: a fake timer that keeps only the
latest callback hands you the second entry's timer, not the stranded one.
The fake collects callbacks in arming order and the test calls armed[0].

Verification: make ci green.

dinstein and others added 2 commits August 10, 2026 07:53
Merger.gen answered "a timer that has begun firing cannot be stopped" for a
settler reset and not for the same race across Flush. It counted per ENTRY and
a fresh entry started at zero, while a coalescer entry is never reset — so its
stranded callback always held zero too, and the two collided:

  a key's timer begins firing (stop returns false); Flush detaches and emits
  that entry; the same key is Added again at generation zero; the stranded
  callback matches the new entry and fires it a window early.

The number now comes from a monotonic sequence on the Merger, so no two armed
timers can ever share one and a stranded callback can match nothing but the
timer it was armed for. That subsumes the settler case rather than replacing
it — the reset path takes its number from the same sequence.

The consequence was mild: one notification arrived early, which the bus
contract already makes safe, since consumers re-read authoritative state
instead of treating an event as a log. Fixed because the mechanism that
prevents it was already there, one level too narrow.

Both races are pinned, driven by a hand-run timer rather than a clock — the
interleaving is the subject, and a sleep would test the scheduler instead.
Verified the new case fails on the previous code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The GAP recorded here a round ago is closed, so the entry states the rule: the
sequence lives on the Merger, so no two armed timers share a number and a
stranded callback can match nothing but the timer it was armed for.

The account of the collision stays, because it is what explains why the
counter is not on the entry — the shape a reader would otherwise reintroduce.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dinstein
dinstein merged commit 7e5fab8 into main Aug 9, 2026
3 checks passed
@dinstein
dinstein deleted the merger-gen branch August 9, 2026 23:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant