Skip to content

id(waiting): WaitingPool::restore ignores the arrived and ready-queue duplicate guards — an already-applied or ready event is held again and handed out a second time #3911

Closed

Description

@dezren39

Summary

WaitingPool::restore (pkgs/id/crates/id-core/src/waiting.rs:312-326) applies a narrower duplicate guard than insert and park: it checks only by_key (already held). insert (waiting.rs:234, guard at :243-246) and park (waiting.rs:273, guard at :281-284) refuse a key that is already held, has arrived, or is sitting in the ready queue. restore checks none of the latter two, so a key the pool already knows is finished or ready-to-hand-out can be held a second time — and later handed out a second time. That is exactly the double hand-out class closed #2001 fixed for insert (which is why the ready_keys index exists, waiting.rs:170-176); the fix never reached restore.

restore is not a test-only path: the event store uses it for every hold. Mirror::hold (pkgs/id/src/event_store/waiting_rows.rs:241) calls pool.restore(...), Mirror::park (waiting_rows.rs:320) re-holds proof headers through it, and the reload-on-open loop (pkgs/id/src/event_store/mod.rs:1015) restores every waiting row through it.

Verified this run (laptop probe, current main 33aefa7)

Probe crate on the laptop (~/git/fleet/probe-2026-11-01-laneA-restoreguard/) #[path]-includes the real waiting.rs from the laptop clone at the same HEAD (md5 9654300d490a030f3f5513c79692e717, identical in both clones) and exercises WaitingPool<u32, &str>:

  • Case A — already-arrived key: arrived(10) then restore(author, 10, [99], ev) → the event is held (len=1, contains(10)=true); a later arrived(99) releases it, handing an already-applied event to the caller a second time. Control: insert of the same arrived key is ignored (len=0).
  • Case B — ready-queue key (id(waiting): WaitingPool::insert's duplicate guard misses the ready queue — the same key inserted twice before take_ready is handed out twice #2001 class): insert(author, 1, [], ev) (goes to the ready queue) then restore(author, 1, [99], ev2) → the same key is also held; arrived(99) releases the held copy and take_ready() returns the ready copy — the same key is handed out twice. Control: park of a ready key is ignored (len(held)=0).

Why it matters

The pool's contract is that a key is in exactly one state — held, ready, or arrived — and is handed out at most once. restore silently breaks that invariant for two of the three states:

Design constraint — one caller does this on purpose

Mirror::present_proof (waiting_rows.rs:288-301) marks a fork-proof header arrived via pool.arrived(...) and then deliberately re-holds it through restore (Mirror::park, waiting_rows.rs:313-323): a proof header never applies, so it is parked for good to keep its conflicts seen. A blind copy of insert's arrived-guard into restore would silently break that park (the header would never be re-held). The fix therefore has to separate the intents — e.g. restore guards the ready queue unconditionally (no caller parks a ready key on purpose) and the arrived case gets an explicit held-for-good route for proofs — rather than one shared guard pasted over all three entry points. This is also why this issue is filed-only, not a trivial self-solve.

Acceptance criteria

  • WaitingPool::restore can no longer hold a key that is in the ready queue, and a non-proof event whose key has already arrived is not re-held/re-released through the Mirror::hold path; the chosen guard shape is documented on restore like the other two entry points.
  • The fork-proof park still works: present_proof/park_proof keep a proof header held for good after its arrival, with a test pinning that behaviour (existing proof-header tests stay green).
  • Regression tests at the pool level, alongside test_restore_keeps_the_cap_and_ignores_a_key_already_held (waiting.rs:728): restore-after-arrived holds nothing and releases nothing when its deps later arrive; restore-of-a-ready-key holds nothing and the key is handed out exactly once.
  • The reload-on-open and Mirror::hold/park paths behave as before for genuinely waiting/parked events (existing waiting + waiting_rows tests stay green).

Dedup

Open/closed searches for "restore WaitingPool duplicate" and "waiting restore" (2026-11-01) return only #2001 (insert's ready-queue guard — closed, restore not covered), #1567 (total cap — closed), and #1730 (arrived-set growth — open, different defect). No issue covers restore's guard.

Activity

  1. added
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    effort/sSmall: expected under 2 hours of work
    on Nov 1, 2026
  2. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · adopted as Phase 3a of design #4000

  4. added
    work/assignedQueue worker lifecycle state
    and removed on Nov 2, 2026
  5. 109 remaining items

  6. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · PR opened

  7. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 4, 2026
  8. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  9. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked verification update

  10. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  11. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 4, 2026
  12. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  13. added a commit that references this issue on Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)queue/claimedQueue worker lifecycle statesource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobstatus/in-progressQueue worker lifecycle statestatus/readyTriaged and ready for queued implementation workstatus/triagedTriaged and routed (see agent note)task/fixhas a fix task queuedtask/small-fixQueue packet kind: bounded fix with reproduction and targeted testswork/assignedQueue worker lifecycle state

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions