Skip to content

Route-bind configure starves on the reader gate: 18-204s ack_ready waits, two distinct mechanisms #243

Description

@iceteaSA

Under parallel-agent load on one root, configure (route bind) sits in the Mutating lane waiting for read_inflight/lsp_inflight to reach zero, and never gets in. Observed waits of 18s → 68s → 82s → 96s → 160s → 204s on a single root. Every phase that does real work reports 0ms:

configure_phase_timings=[canonicalize=0ms, worktree_probe=0ms, config_resolve=0ms, ack_ready=95989ms]
blockers=[queued_behind_configure(1), waiting_on_readers]

No slow project walk, no gitignore scan, no DB open. All of it is admission wait.

Because the daemon's route-bind relay times out well before that, new binds fail while already-bound routes keep working — so it presents as "aft is broken for new sessions" while existing sessions are fine.

Two distinct mechanisms, not one

The blocker census distinguishes them, and the split matters because a single fix only addresses one:

16x  waiting_on_readers            <- reader churn: many short reads, none individually old
 3x  waiting_on_readers_stuck(...) <- one long-lived reader, named

(a) Reader-churn starvation — 16 occurrences, the 68–96s band. Admission is reader-preferring with no fairness (executor/mod.rs:2078,2090):

Lane::PureRead => actor.read_inflight < config.read_cap && actor_has_capacity,
Lane::Mutating => !has_epoch_reader && actor_has_capacity,

A new PureRead is admitted whenever the cap allows. Nothing consults the Mutating queue, so a continuous stream of short reads keeps read_inflight above zero indefinitely and the queued writer is lapped forever. Self-reinforcing: more parallel sessions → denser reads → longer starvation.

A write barrier already exists but is scoped one class too narrowly (executor/mod.rs:~1200):

fn higher_priority_writer_barrier_blocks(&self, job_class: JobClass) -> bool {
    matches!(job_class, JobClass::Maintenance)
        && !self.interactive.queue(Lane::Mutating).is_empty()
}

A queued interactive writer blocks maintenance from starting, but not interactive reads. The concept is in the codebase; it just doesn't cover the case that starves.

(b) A single long reader on the LSP lane — 3 occurrences, and they are the WORST waits (160s, 204s). All three name the same lane:

waiting_on_readers_stuck(job=subc-56-84  command=executor::Interactive::SerialLspStatus
                         lane=SerialLspStatus root=/…/openai-auth age_ms=66289)
waiting_on_readers_stuck(job=subc-82-389 command=executor::Interactive::SerialLspStatus
                         lane=SerialLspStatus root=/…/openai-auth age_ms=62200)

has_epoch_reader includes lsp_inflight, so one 60s+ LSP-status job gates the Mutating lane on its own. Write-preferring admission does not fix this — with a single reader holding for 66 seconds, the writer waits 66 seconds no matter how it is prioritised.

Why the split is the point

The obvious fix (make admission write-preferring or fair) resolves (a) and leaves (b) — which produced the two longest stalls observed. Worth treating as two items:

  1. Fair/write-preferring admission: extend the existing barrier so a queued interactive Mutating job blocks new PureRead admissions. Needs a bound so a permanent write barrier can't stall reads indefinitely, but the mechanism is already there for maintenance.
  2. Bound or off-lane the long LSP-status reader: a 60s+ SerialLspStatus job holding lsp_inflight blocks every route bind on the root. Either bound it, or let it wait without holding the epoch-reader gate — the same shape as LspManager mutex is held across the post-edit diagnostics wait (up to 10s), serializing every other LSP operation #216's parked-waiter fix.

Relationship to #242

Sibling, not duplicate. #242 is a running writer (blocking-fresh inspect) holding the lane while unrelated mutations queue. This is a queued writer that can't get the lane because readers never yield. Same admission design, opposite ends; different fixes.

The two also compound: the 60s+ SerialLspStatus reader here and the 63s inspect in #242 are on the same root, in the same window.

Environment and attribution

v0.51.2 (aa027c6f), Linux, subc daemon transport, ~9 concurrent sessions across ~6 roots, several parallel implementer agents on /…/openai-auth invalidating files every 1-2s. Not resource pressure: 38G RAM available, load 3.04, daemon at 2.6G.

Starvation mechanism (a) was diagnosed independently by a peer operator working the daemon side, from the ack_ready/blockers timings; I verified it against the admission code and added the (b) split from the stuck-reader census.

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