Skip to content

revert(daemon): dispatch broker requests on the connection task again - #4078

Merged
ScriptedAlchemy merged 2 commits into
masterfrom
refactor/subtract-broker-dispatch
Oct 16, 2026
Merged

ScriptedAlchemy merged 2 commits into
masterfrom
refactor/subtract-broker-dispatch

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Why

A request parked on a project open, the writer gate, or a generation decode gives its admission slot back again, so parked requests cannot starve new calls into bulk_capacity_reached.

#4065 moved every broker request's dispatch onto a blocking-pool thread with native_runtime::off_native_thread. park_admission finds the connection's lease through the Tokio task_local! CONNECTION_ADMISSION scopes that with_connection_admission installs on the connection task (core_admission.rs, tracedecay-code-index-runtime/src/ports.rs). A future polled on another thread sees no scope, try_with fails, and the park keeps its slot. The move also put whole handler futures on 2 MiB pool stacks, dropped disconnect cancellation, and showed no latency gain. The long-poll failure it worked around goes away in #4076, which counts long native polls instead of failing them.

Scope

  • Reverts merge ab27bb6d5f (fix(daemon): dispatch broker requests off native workers #4065): the off_native_thread(execute(request)) wrap in drive_retained_invocation_responses, the 'static dispatch closures added for the move, and long_dispatch_tests.
  • Adds broker_dispatch_admission_tests::a_dispatch_parked_on_a_barrier_releases_its_admission_slot. It drives a real broker connection under with_connection_admission with one general slot, parks the dispatch in park_admission, and expects the slot back while it is parked.
  • Out of scope: perf(daemon): serve Work/Workflow requests off native workers #4052's off_native_thread sites (invocation/work.rs off_native_worker, observability_producer.rs work_fan_out_binding) stay. Neither moved future reaches park_admission or reads a task-local: Work and Workflow dispatch call synchronous storage and session evidence ports, none of the code-index or project-open park sites. That move removed 594 native writer waits. Its one loss is span parentage for child spans of daemon.service.work.execute, which no behavior reads.

Blast Radius

Broker dispatch runs on the connection task again, as it did before #4065. Long dispatch polls are logged and counted by #4076 instead of failing the connection. Merged without #4076, a debug build's long dispatch poll can still trip the old guard, as it did before #4065.

Verification

  • The new test fails on master with the parked dispatch kept its connection's admission slot (left: 0, right: 1) and passes after the revert.
  • //crates/tracedecay:unit_test (897 tests), //crates/tracedecay:daemon_suite (77), and //crates/tracedecay:mcp_suite (666) pass. --config=clippy and rustfmt are clean for tracedecay.

This reverts merge ab27bb6 (#4065). It polled each broker request on a
blocking-pool thread through native_runtime::off_native_thread. That thread
runs outside the connection task's task-local admission scopes, so
park_admission could not find the lease and a request parked on a project
open, the writer gate, or a generation decode kept its admission slot. The
hop also moved whole handler futures onto pool threads with smaller stacks,
lost disconnect cancellation, and measured no latency gain. The long-poll
failure it worked around is gone: long native polls are now counted, not
failed.
@changeset-bot

changeset-bot Bot commented Oct 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7643104

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Devin Review

)
}
},
Ok(request) => execute(request).await,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Long dispatch drops broker responses

When execute(request) uses over 250 ms of CPU in one poll, LongPollGuard still panics in debug builds. The connection closes without answering the request.

Learn more

Broker connections run as native tasks. LongPollGuard still asserts when a native task poll exceeds LONG_POLL_CPU (250 ms). Awaiting dispatch directly makes its synchronous CPU work part of the connection task's poll again. The removed long-dispatch test exercised exactly this case; the assertion prevents the response from being written.

Example: A broker request spends 300 ms of thread CPU inside dispatch before its next await. The native task panics at 250 ms and the waiting client receives a closed socket instead of its response.

Recommended fix: Preserve connection-task admission scope without subjecting synchronous dispatch to the fatal native-poll guard. Change the guard to nonfatal counting as planned, or isolate CPU-heavy work while retaining the admission lease, and retain a regression test for long dispatch.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

)
}
},
Ok(request) => execute(request).await,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Writer waits stall broker workers

When execute(request) reaches a synchronous writer wait, it parks a native worker until the writer replies. recv_writer_reply cannot yield that worker, so concurrent waits delay unrelated socket responses and deadlines.

Learn more

The foreground broker runs each connection on a native worker. Synchronous storage ports reach recv_writer_reply, which calls recv_blocking directly outside a Tokio multithread runtime. A writer queue wait therefore occupies the native worker rather than yielding it. Moving the entire dispatch off native workers previously prevented this; running it inline reintroduces native worker starvation under concurrent writer waits.

Example: If all native workers dispatch requests whose synchronous ports wait on a busy writer, none can poll another accepted socket or timer until a writer reply arrives.

Recommended fix: Keep park_admission on the connection task, but move synchronous storage operations to blocking threads or introduce asynchronous writer waits at their ports. Verify native writer-wait counts under concurrent broker requests.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@ScriptedAlchemy
ScriptedAlchemy merged commit aa99fa1 into master Oct 16, 2026
17 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant