Skip to content

fix(daemon-service): write scope-set replicas on the blocking pool - #4068

Merged
ScriptedAlchemy merged 1 commit into
masterfrom
refactor/3736-t4-scope-set-cas
Oct 16, 2026
Merged

ScriptedAlchemy merged 1 commit into
masterfrom
refactor/3736-t4-scope-set-cas

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Part of #3736

DaemonInvocationService::compare_and_swap_scope_set wrote every scope-set replica with the synchronous AuthorizedScopeSetSqliteStorage::compare_and_swap inside the async request. Since #4003, daemon invocations run on native workers. Each of those writes waits on the SQLite writer, and that wait parks the native worker for the whole write (the native_thread_writer_waits hazard). This PR runs the replica loop on native_runtime::spawn_blocking. Once the first write starts, the loop still finishes every replica. Dropping the request now only abandons the wait, so a partial effect cannot result.

Measured

A scratch build logged every writer wait with its thread and caller frames. That build also routed the in-process invocation executor onto native tasks, so mcp_suite drives the multi-root routes on native workers. Neither scratch change is in this PR. mcp_suite then recorded:

  • without this fix: 7 waits on native workers, all from DaemonInvocationState::invoke_for_project. They ran through AuthorizedScopeSetSqliteStorage::compare_and_swap and its begin_transaction/execute.
  • with this fix: 0 waits on native workers. Every remaining wait is on a blocking-pool thread.

Verified at d4e8c589b1

  • //crates/tracedecay:unit_test (including multi_root_journey's scope-set CAS tests) passed in 153.8s, :mcp_suite in 120.2s, :daemon_suite in 122.0s and :session_suite in 61.3s.
  • //crates/tracedecay-daemon-service:unit_test passes. Its first run, at host load average about 1000, failed five tests on SQLite lease expiry and interrupted queries in tests that never reach this code. The rerun passed in 18.7s.
  • bazel build --config=clippy passes over //crates/tracedecay-daemon-service/.... rustfmt_check_2024 and scripts/check-ambient-tokio.py pass.

Devin Review

compare_and_swap_scope_set wrote each replica through the synchronous scope-set storage port inside the async request. On a native worker every write parked the worker on the SQLite writer. Run the replica loop on native_runtime::spawn_blocking; the loop still finishes every replica once it starts.
@changeset-bot

changeset-bot Bot commented Oct 15, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: d4e8c58

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 1 potential issue.

Devin Review

Comment on lines +288 to +290
let next = tracedecay_runtime_core::native_runtime::spawn_blocking(move || {
for storage in storages {
match storage.compare_and_swap(expected_revision, &next).ok()? {

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.

🔴 Cancelled scope sets commit after admission

When the blocking pool delays spawn_blocking, a cancelled or expired scope-set request still writes every replica. spawn_blocking keeps queued work alive independently of the request, and the worker never rechecks its controls.

Learn more

The request checks its cancellation and deadline immediately before queuing the write, but a blocking-pool worker can start later. The blocking runtime queues work immediately and keeps it alive even if the returned future is dropped. Neither control is checked by the worker before its first compare-and-swap. The request dispatcher can report cancellation or timeout, and in-process settlement can stop waiting while the queued work remains pending.

Example: A scope-set create passes the deadline check at 12:00:00. The pool starts its worker after the deadline at 12:00:45. The request has timed out, but the worker creates revision one anyway; a retry with the same expected revision now conflicts.

Recommended fix: Move the final deadline and cancellation check into the blocking closure immediately before its first CAS. Preserve the rule that, after the first CAS starts, every replica is attempted even if the request is subsequently cancelled.

Devin Review


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

@ScriptedAlchemy
ScriptedAlchemy merged commit 8c7d3ed into master Oct 16, 2026
17 of 21 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