Skip to content

[DV-05] [P1] Keep bridge response backpressure inside the bounded worker lifecycle #565

Closed

Description

@wsdt

Priority: P1
Area: Bridge transport
Repository / target branch: wavect/semaprax / wavect/v090
Reviewed commit: 5428b0805fa3cf35859c843a53e30c3252ef378c
Review date: 6 October 2026

Defect

A bridge/invoke worker calls registry.settle before it writes its reply. Settlement removes the request from the table that enforces MAX_IN_FLIGHT = 4. The worker then takes a shared output mutex and performs a blocking writeln!/flush.

If the client keeps sending requests but stops reading stdout, the first response eventually blocks on the full pipe and later response workers block on the output mutex. Their registry entries have already been removed, so the serving loop can continue spawning new workers for additional calls. The nominal four-call gate no longer bounds outstanding response workers or their retained result/ID buffers. A slow or stalled consumer can cause unbounded thread/memory growth; actual OS thread exhaustion need not be induced to demonstrate the escape.

EOF/shutdown also cannot finish while these workers are blocked. EndOfSession cancels only the still-live registry entries, which are empty for these workers; the Rust scope then waits for all response writers to return. A successful but blocked write does not set broken_flag, so the error-only cancellation logic does not help.

Executed reproduction

The test pre-handshakes a real Server, then calls the production serve_with with a barrier-controlled Write implementation representing a consumer that stops draining stdout. It sends 12 sequential invokes with invalid params, using each registry settlement as a barrier before sending the next request. No adapter, external service or slow provider is involved.

Observed on Linux:

  • MAX_IN_FLIGHT: 4.
  • Settled invocations while the writer remains blocked: 12.
  • registry.live(): 0.
  • Process thread count: 4 before, 16 during.
  • EOF does not complete the session until the test releases the output writer.

After releasing the writer, every fixture worker and session exits. The test avoids exhausting the machine and makes no claim that a real production process was crashed. The production source was copied byte-for-byte with only a test module appended.

Smallest useful change

Account for each spawned worker through its response-delivery lifetime, including queued/blocked output. Releasing the provider-call slot may remain separate, but it cannot release all ownership that bounds a still-running reply worker. Introduce a small bounded response capacity/permit or reuse existing admission ownership; do not allow completed replies to accumulate unboundedly behind the output mutex.

For the supported stdio entry point, make stalled output an observable, bounded transport state so session stop can cancel live adapter work and finish according to a defined policy. Preserve response correlation and already-dispatched side-effect uncertainty if delivery fails. A generic arbitrary Write can block forever; do not merely advertise a bounded guarantee over such an object without a cancellable/bounded contract or an enforceable stdio implementation. Avoid adding an unrelated transport framework.

Acceptance criteria

  • A blocked output consumer cannot create more outstanding worker/response ownership than the documented bound.
  • Repeated fast refusals and successful replies are both covered; a provider-invoke limit alone is insufficient.
  • Stalled/failed output triggers the defined session-end policy and does not prevent cancellation/settlement of unrelated live adapter work.
  • The installed stdio path has a real pipe-backpressure regression with finite outer watchdog and test-controlled cleanup.
  • Generic serve_with embedding behavior clearly states or enforces its output cancellation/bounding requirements.
  • Responses retain the correct IDs, terminal states and dispatch uncertainty; stopping output never silently makes a possibly executed operation retryable.
  • Healthy slow readers can resume within the supported allowance without duplicate execution or unbounded buffering.

Existing issue boundary

#476 handles transport operations that return an error, ensuring cancellation happens before joining workers. This case has no I/O error: Write remains blocked after the registry slot has been released. #454 required bounded concurrent cancellation/pipe handling but did not supply this response-worker ownership behavior. The existing early cancellation guard and frame-size limits should remain unchanged.

Verification handoff

Read AGENTS.md and docs/HARNESS-BRIDGE-V1.md. Extend the existing bridge lifecycle tests and use a barrier/capacity-controlled writer before a real bounded pipe test. Record the fixing commit, executed counts and ordinary/error/EOF cases. Keep tests local and bounded rather than forcing machine-wide resource exhaustion.

Pinned source references

Activity

  1. wsdt commented on Oct 6, 2026

    @wsdt
    MemberAuthor

    Fixed in b5e3f37 (pushed to main). Root cause: registry.settle freed the in-flight slot before the blocking reply write, so nothing bounded response workers. Slots are now owned until the reply is delivered or fails (MAX_IN_FLIGHT, SPX-HPN012); an output-stall policy (default 30s, SEMAPRAX_BRIDGE_OUTPUT_STALL_MS) ends a stalled session with TimedOut; the installed --stdio path writes through a bounded detached writer on a dup'd fd. Evidence: bridge_lifecycle::backpressure tests (at most MAX_IN_FLIGHT+1 frames read while output blocked; exact one reply per id after resume; serve_with and serve_detached TimedOut; real --stdio child exits within a watchdog when its client stops reading); cargo test -p semaprax-harness green, clippy -D warnings clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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