Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions crates/tracedecay-daemon-service/src/invocation/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,21 +282,25 @@ impl DaemonInvocationService {
// first durable write. Once the first compare-and-swap starts, finish
// every authorized replica so the caller receives one authoritative
// settlement instead of a fabricated interruption over a partial
// effect.
for storage in storages {
match storage
.compare_and_swap(request.expected_revision, &next)
.ok()?
{
tracedecay_store::runtime::ScopeSetCasOutcomeV1::Applied(_) => {}
tracedecay_store::runtime::ScopeSetCasOutcomeV1::Conflict { .. } => {
let stored = storage.read(next.scope_set_id()).ok()?;
if stored.as_ref() != Some(&next) {
return None;
// effect. The writes park their thread on the writer, so they run on
// the blocking pool.
let expected_revision = request.expected_revision;
let next = tracedecay_runtime_core::native_runtime::spawn_blocking(move || {
for storage in storages {
match storage.compare_and_swap(expected_revision, &next).ok()? {
Comment on lines +288 to +290

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.

tracedecay_store::runtime::ScopeSetCasOutcomeV1::Applied(_) => {}
tracedecay_store::runtime::ScopeSetCasOutcomeV1::Conflict { .. } => {
let stored = storage.read(next.scope_set_id()).ok()?;
if stored.as_ref() != Some(&next) {
return None;
}
}
}
}
}
Some(next)
})
.await
.ok()??;
Some((
active_scope,
MultiRootScopeSetCasResultV1 {
Expand Down
Loading