Skip to content

Three bounds that don't bind: unbounded executor queues, a soft deadline that doesn't bound the scan, and a cancellation gap around blocking git spawns #221

Description

@iceteaSA

Three separate findings that share a shape — a mechanism that reads like a limit but doesn't constrain the thing you'd expect. Filed together because the shape is the point; the fixes are independent.

Line numbers against 83fe3bd5.


1. Executor job queues have no length cap · P2

// executor/mod.rs:1252
fn push_job(&mut self, lane: Lane, job: QueuedJob) {
    self.order.push_back(lane);
    self.queue_mut(lane).push_back(job);
}

Unconditional push_back, and submit_with_completion_cancellable pushes without checking depth. maintenance_cap bounds concurrent in-flight maintenance jobs, not the queued backlog — so it limits how many run, not how many wait.

ctx.configure_maintenance_jobs (context.rs:2715) is likewise a bare push_back.

The reason this isn't theoretical: maintenance drain jobs self-requeue when they hit their batch cap (requeue_kind: drained.has_more.then_some(kind)). Under sustained watcher or LSP event pressure that's a continuous enqueue stream against a queue with no ceiling.

Fix direction: cap the per-actor queued count (drop-newest, or reject with backpressure), and/or coalesce duplicate drain kinds already queued — a second Watcher drain sitting behind an identical one is pure backlog.

2. soft_deadline bounds the waiting caller, not the scan · P3

The field has exactly one use:

// inspect/manager.rs:1831
let timeout = after(self.soft_deadline);

That's the caller's wait in wait_for_outcome. When it expires the caller gets Stale/Pending — but the scan keeps running to completion in the background pool with no deadline and no cancellation.

That's defensible by design (the result gets cached and reused), and I'm not arguing the behaviour is wrong. The problem is the name: "soft deadline" reads as a bound on the work, and the default is 1s while real Tier-2 scans on a non-trivial root routinely run far longer. Anyone reasoning about worst-case background cost from this field will be wrong.

Fix direction: either thread it into the scan (check between phases, abort early, cache partials) or rename it to something like caller_wait_timeout and document that background work is unbounded. The rename is the cheap honest option.

3. A blocking git spawn sits between two cancellation checks · P3

configure_cancelled is checked at phase boundaries (configure.rs:1776, 1843, 1871, 2187, 2218, 2259). Between the check at 1843 and the one at 1871:

// configure.rs:1843
if let Some(cancelled) = configure_cancelled(&req.id) { return cancelled; }
// configure.rs:1846
let (is_worktree_bridge, git_common_dir) = detect_worktree_bridge(ctx, &canonical_cache_root);

detect_worktree_bridge spawns a blocking git subprocess (configure.rs:758). The same shape appears around the artifact-cache-key path, where repo_root_commit_with_retry can spawn git rev-list up to three times with sleeps between retries.

A Goodbye or bind-deadline expiry arriving during those spawns can't land until the subprocess returns. On a slow filesystem or a very large repo that's seconds of uninterruptible time — the cooperative cancellation is delayed by exactly the duration you'd most want to cancel.

Bounded and first-configure-only (both are memoized after), which is why it's P3.

Fix direction: check configure_cancelled immediately before and after each blocking spawn, or move the git probes onto a cancellable path.


Provenance and limits. All three came out of a read-only sweep; I verified each against upstream/main before filing — the unconditional push_back, the single soft_deadline use site, and the git spawn sitting between the two checks. None are measured. I have not observed a queue growing, a scan overrunning, or a cancel being delayed in practice — these are structural readings of what the code permits, not reports of it happening. #1 is the one I'd expect to matter; #2 may be a doc fix rather than a code fix, and that's your call.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions