Skip to content

A waiter that times out while its lease is being written gets a timeout, and the lease is kept #402

Closed

Description

@V3RON

Found while reviewing #397 (task #364).

On the daemon's grant path, the waiter's timeout timer stays armed while Registry.createLease writes the lease (src/leasing/lease-acquisition-coordinator.ts:669-674). If it fires in that window, the caller gets QueueTimeoutError but the lease exists: the device stays leased to nobody until the lease expires. #grant ignores the result of queue.resolve. This happened on main before #397 too; since #397, a repeat under the same idempotency key at least gets that lease back.

Written by an agent.

Activity

  1. added
    bug:newReported. Nobody has looked yet.
    on Oct 6, 2026
  2. added
    bug:triageAn agent may reproduce it and write the triage report.
    and removed
    bug:newReported. Nobody has looked yet.
    on Oct 16, 2026
  3. self-assigned this
    on Oct 16, 2026
  4. V3RON commented on Oct 16, 2026

    @V3RON
    ContributorAuthor

    Reproduction

    Confirmed on origin/main (1b5537da). On bug/402-repro (0d9ee5e0),
    lease-acquisition-coordinator.test.ts: "delivers the grant to a queued waiter
    whose queue timeout falls due while its lease is being written".

    pnpm exec vitest run --project unit src/leasing/lease-acquisition-coordinator.test.ts \
      -t "delivers the grant to a queued waiter"
    -   "callerError": undefined,
    +   "callerError": "QueueTimeoutError",
    -   "outcome": "fulfilled",
    +   "outcome": "rejected",
    

    The body is right that a repeat under the same key gets that lease back. Wider damage:
    that lease belongs to a caller told it timed out, and blocks its next one.

    retry=RequesterAlreadyLeasedError records=[["holder","granted"],["waiter","granted"]]
    

    Root cause

    Defect. lease-acquisition-coordinator.ts:874 grants a ready device without
    marking the waiter processing, so it is still queued while #grant awaits the
    write (:915). WaitQueue#armTimeout (wait-queue.ts:377) spares only a waiter
    no longer queued, so the timer rejects this one mid-write. The grant is dropped:
    queue.resolve (:931) returns false, unread. Other paths mark first (:722,
    :887).

    Simplest fix

    Mark processing before the write, and re-queue a failed write as #grantPhysical
    does (:762-779) — four lines in #decide:

    if (!this.options.queue.markProcessing(waiter)) return undefined;
    try {
      await this.#grant(waiter, plan.device, plan.kind);
    } catch (error: unknown) {
      this.#logFailure("granting a ready device failed", waiter, plan.device, "grant", error);
      this.#defer(waiter);
    }

    A waiter timed out mid-attempt keeps its lease and sees the grant — the queue's own
    rule for that state (wait-queue.ts:339), not new policy. src/leasing green with
    it: 590 passed.

    Alternatives rejected

    • End the lease the write made: a lease ends only by release or TTL, and the device is
      reclaimed after a grant was promised.
    • Release on a false from queue.resolve: the caller still times out over a device it
      could have used.
    • Cancel this timer before the write: two places arm a deadline wait-queue.ts:332 says
      is armed once.

    Risk

    Side findings

    Written by an agent.

  5. V3RON commented on Oct 16, 2026

    @V3RON
    ContributorAuthor

    Handoff

    Done — Reproduced on origin/main (1b5537da) as a failing unit test on
    bug/402-repro (0d9ee5e0, one file, no PR): "delivers the grant to a queued waiter
    whose queue timeout falls due while its lease is being written". Fails on a named
    assertion; pnpm exec vitest run --project unit src/leasing gives 1 failed, 589
    passed. Report posted above.

    Not done — the fix, and turning the repro into the closing PR's regression test
    (rule 9).

    Findings — the outcome is not an open decision: wait-queue.ts:339 says a timeout
    firing mid-attempt must not reject, and three paths already mark processing before one
    starts. Only grant-ready does not. The proposed fix's catch is load-bearing — with
    markProcessing alone, a createLease that throws leaves the waiter pending, untimered
    and not-cancellable.

    Blocked on — nothing. Maintainer decides bug:ready.

    Written by an agent.

  6. removed their assignment
    on Oct 16, 2026
  7. added
    bug:readyAn agent may fix it.
    and removed
    bug:triageAn agent may reproduce it and write the triage report.
    on Oct 16, 2026
  8. self-assigned this
    on Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bug:readyAn agent may fix it.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions