fix(telegram): fence delete settlement to its dispatched epoch and keep rollback route-atomic - #3617
fix(telegram): fence delete settlement to its dispatched epoch and keep rollback route-atomic#3617twoimo wants to merge 1 commit into
Conversation
…ep rollback route-atomic settleDelete() previously matched only topic id plus delete_pending, so a held generation's definite remote result could settle a newer re-fenced delete for the same session and release its topic-id quarantine. It now requires the caller's dispatched authority epoch to equal both the record's epoch and the registry's current epoch, and all four daemon call sites capture that epoch before dispatching the remote delete. restoreDeleteFence() reinstated a pending collision without rebuilding the derived routing tables, and its narrow byTopic.delete never ran on the absent-record branch that settlement creates. It now rebuilds inbound routes after writing the epoch, so a restored fence re-quarantines the topic id instead of leaving inbound routing open to the collision partner. Settlement also rebuilds routes on success so a settled topic id becomes adoptable and a surviving colliding record becomes routable immediately.
|
Rebased onto current dev This is the successor to #3610 and implements both P1s from your review exactly as specified, rather than re-proposing the one-line happy-path change: P1-1 — P1-2 — Both regressions you asked for are included — out-of-order E1/E2 settlement, and persist-failure active+pending collision. I verified each fails for its own blocker by reverting only that guard, not just with everything stashed, so neither passes for the wrong reason. Ready for review at your convenience. |
Yeachan-Heo
left a comment
There was a problem hiding this comment.
Exact-head hostile review: REQUEST_CHANGES
Reviewed head: 2596b98b28df8177fda3eab3d6f092d1f47c285d
Exact/current base: 7f5167c864f938558840084ad218d48132f0a67a
P0: 0. P1: 4.
- Epoch advancement can exceed
Number.MAX_SAFE_INTEGER; E1/E2 then collapse to the same IEEE-754 value and stale settlement can clear the newer fence. Use checked advancement/saturation and reject unsafe dispatched epochs. restoreDeleteFencecan overwrite a newer E2 with an old E1 snapshot after stale E1 settlement/persist failure. Rollback must CAS the exact expected post-settlement state, and refused settlement must not restore.- Settlement rebuilds/routes before the final clear is durable. During a held write, a colliding survivor becomes routable; a later persistence failure re-quarantines too late. Keep a transient clear-pending quarantine or publish the route only after durable commit.
- The first revoked-lease compensation path marks remote compensation complete before registry clear persistence. If that write fails, outer recovery skips fence supervision, leaving live memory cleared while disk retains
delete_pending. Treat remote success + settle + durable clear transactionally.
Exact CI is terminal green (17 success / 5 skipped / 0 bad), but lacks overflow, stale rollback CAS, held-write inbound, and first-compensation persistence failure coverage. Normal E1/E2 checks and route rebuild-on-restore are improved, but these remaining authority/durability windows are material.
—
[repo owner's gaebal-gajae (clawdbot) 🦞]
Successor to #3610, addressing both P1 findings from that review. Thank you for the precise diagnosis — both races were real, and the one-line happy-path repair was indeed incomplete.
P1-1 — settlement is now fence-epoch bound
settleDelete(sessionId, topicId)checked only topic id plusdelete_pending, so a held E1 delete could settle a newer E2 fence for the same session/topic and release the newer quarantine.settleDeletenow takesdispatchedAuthorityEpoch— the epoch the caller held when it dispatched the remote delete — and requires equality against both the record'sauthorityEpochand the registry's currentauthorityEpoch(sessionId). The second comparison is what actually closes the race:beginDeletebumpsepochsand the record together, so a superseded E1 holder fails the registry-current check.All four daemon call sites capture the epoch before dispatch:
getOrCreateTopiccreate callbackfenceAcceptedCreateForLease's returned recorddeleteTopic,user_createdtopic (no remote dispatch)beginDeletedeleteTopic, afterdeleteForumTopicreturns definiteP1-2 — rollback is now route-atomic
restoreDeleteFencereinstated the pending collision but used a narrowbyTopic.delete(...)that only ran in the record-present branch — never in the absent-record branch, which is exactly the branch settlement creates. It also never re-added the id to the ambiguity set, so inbound routing stayed open despite the restored fence.It now calls
rebuildInboundRoutes()after the epoch write, matching the patternrestoreDeleteAuthorityalready used.Verification
Three regressions in
test/notifications-topic-settle-fence-epoch.test.ts, each proven to fail without its specific repair:expect(reg.settleDelete("A", "42", dispatchedEpochE1)).toBe(false)receivestrue.restoreDeleteFencereverted to the narrow form,expect(reg.sessionForTopic("42")).toBeUndefined()receives"B"— verbatim the open inbound route you described.All three fail on unmodified dev and pass with the fix. Wider sweep: 568 pass / 0 fail across the topic-registry, new, and main telegram daemon suites; 704 pass / 0 fail across all 15 telegram/topic files; generation guard 41 pass;
tscandbiomeclean.Rebased onto dev
7f5167c86. Tests use only the exportedTopicRegistrysurface — no private access, no casts. Happy to keep iterating if either fence needs to be stricter.