Skip to content

id(waiting): WaitingPool::insert's duplicate guard misses the ready queue — the same key inserted twice before take_ready is handed out twice #2001

Closed

Description

@dezren39

Summary

WaitingPool::insert in pkgs/id/src/waiting.rs promises that a duplicate is ignored — "An event whose key is already held or has arrived is a duplicate, and it is ignored" (insert docs, waiting.rs:180-184) — but the guard misses the one state an event can be in that is neither held nor arrived: ready. An event whose dependencies have all arrived is pushed onto the ready Vec (waiting.rs:206-211) without recording its key anywhere. Inserting the same key a second time before take_ready() is drained passes both guard checks (arrived.contains(&key) and by_key.contains_key(&key) are both false) and pushes a second copy, so take_ready() returns the same event twice and the caller applies / decides it twice.

Evidence (verified by code reading on main 77918e0, 2026-10-28)

  • insert (waiting.rs:193-212): the duplicate guard checks only self.arrived and self.by_key. The ready path (missing.is_empty()) does self.ready.push(event) and records the key in no index — not by_key, not arrived, no ready-key set exists in the struct (waiting.rs:124-141).
  • Deterministic trace with the pool's own test vocabulary (WaitingPool<u32, &str>):
    1. pool.arrived(10, 0) — dependency 10 is present.
    2. pool.insert(author(1), 1, [10], "first", 0) → ready = ["first"].
    3. pool.insert(author(1), 1, [10], "second", 0) → guard passes again → ready = ["first", "second"].
    4. pool.take_ready() returns both copies of key 1.
      The no-deps form (insert(author, 1, [], …) twice) behaves the same.
  • The existing duplicate test does not cover this state: test_a_duplicate_key_is_ignored (waiting.rs:578-589) pins the held-key duplicate and the arrived-key duplicate only; no test inserts a duplicate while the first copy is sitting in ready.
  • Caller impact today is bounded but real: the event store's drain (src/event_store/ingest.rs:725, 742) extends its work queue with take_ready() after every settle, so a duplicated ready event is decided a second time and only the store's own duplicate detection (Settled::Duplicate) keeps it harmless. The pool's documented contract is what other callers (the module is pub, and its caller-loop protocol in the module docs, waiting.rs:16-24, tells callers to apply what take_ready returns) rely on.

Related but distinct: #1730 (the arrived set grows unboundedly) and #1567 (no total cap on held events) — neither covers the ready-queue duplicate.

Why it matters

The waiting pool is the causal-order gate for event ingest: everything it returns is treated as "apply this, once". A pool that can hand the same event out twice breaks its own stated contract and pushes duplicate defence onto every present and future caller. The fix is small and the module is pure (no I/O, explicit clock), so a regression test is cheap and deterministic.

Acceptance criteria

  • WaitingPool::insert (and park, if it shares the path) ignores a second insert of a key that is already in the ready queue, matching the documented duplicate rule for held and arrived keys.
  • A regression test alongside test_a_duplicate_key_is_ignored covers the ready-state duplicate (deps already arrived, and the no-deps form), asserting take_ready() returns the event exactly once.
  • Existing waiting-pool tests still pass (cargo test --lib waiting or the crate's equivalent targeted run).
  • No change to held / parked / restore semantics or to the per-author cap.

Filed by new-issue-creator-code-a (Lane A), 2026-10-28 run. Verification: code reading + deterministic trace on main 77918e0; no probe crate was compiled (per the laptop-only probe rule, and the trace needs no execution to settle — every step is a direct read of insert's guard and push).

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    priority/normalOrdinary priority (the default)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  3. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Nov 1, 2026
  5. 26 remaining items

  6. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    on Nov 1, 2026
  7. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — queue task 2918 (lane A, shift close-out) — targeted test PASSED

    Update after the park: the laptop run left going at park time has finished — cargo test --features web --lib waiting in ~/git/fleet/2918-2001-waiting-ready: test result: ok. 38 passed; 0 failed (log: laptop /tmp/2918-waiting-test.log, EXIT:0).

    The resuming lane can go straight to the merge step: PR #3357 (head 20a8d5b, Codex clean, no findings) → merge → close #2001 → complete task 2918 as fixed_merged.

  8. 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 1, 2026
  9. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up (resume)

  10. added a commit that references this issue on Nov 1, 2026
  11. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

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)source/creatorFiled by the new-issue-creator jobstatus:donetask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions