fix(lsp): detach workspace/diagnostic/refresh from notification handlers to prevent deadlock (#25438)
## What this PR does Fixes the permanent language-server deadlock reported in #24955. `workspace/diagnostic/refresh` is a server-to-client *request*, and a spec-compliant client may re-pull `textDocument/diagnostic` from this same server before replying to it. Three notification handlers (`initialized`, `did_change_configuration`, `did_change_watched_files`) awaited that round-trip inside the handler itself, pinning one of the transport's concurrency slots (tower-lsp-server defaults to 4) for as long as the client took to reply. Four rapid watcher events — e.g. four saves of `.oxlintrc.json` or any `tsconfig*.json` in pull-diagnostic mode — pinned all four slots on handlers waiting for the client, while the client waited on diagnostic pulls the server could no longer service. Circular wait, no timeout, no recovery until the process is killed. The refresh is advisory — nothing consumes its result beyond logging a warning — so this PR detaches it: a new `Backend::spawn_diagnostic_refresh()` clones the client and spawns the round-trip, returning the handler's slot immediately. The server keeps answering requests with any number of refresh replies outstanding. This is the issue's first suggested fix; raising the concurrency level (#25153) only widens the window rather than closing it, as the issue notes. ## How it's tested `test_outstanding_diagnostic_refreshes_do_not_wedge_the_server` runs in the crate's existing wire-level `TestServer` harness — real LSP framing over duplex streams, the real `Server` at its real default concurrency of 4. It initializes in pull mode with dynamic watchers, fires four watched-file events, reads each resulting `workspace/diagnostic/refresh` request **without replying** (what a busy client looks like), then sends `shutdown` and requires a response within a 10s timeout. Verified both directions: against the previous code the test fails by timeout at exactly the reported wedge point; with the fix, the full crate suite passes (85/85). `cargo fmt` and `clippy --all-targets` are clean for the changed files. The only other change is adding tokio's `time` feature to dev-dependencies for the test's timeout. ## AI disclosure Investigated, implemented, and verified with AI assistance (Claude Code), per the contributing guide's disclosure policy. I traced the root cause to the transport's concurrency accounting, confirmed the circular wait matches the reporter's reproduction, and validated the fix with the deadlock-reproducing regression test above before submission. Co-authored-by: Alexander S. <sysix@sysix-coding.de>
H
Hamody We committed
d893fd4cabf37189a23d134d5d72958675f61b69
Parent: e5c38c9
Committed by GitHub <noreply@github.com>
on 8/23/2026, 11:22:58 PM