Skip to content

fix(lsp): detach workspace/diagnostic/refresh from notification handlers to prevent deadlock - #25438

Open
hamodywe wants to merge 1 commit into
oxc-project:mainfrom
hamodywe:fix/lsp-diagnostic-refresh-deadlock
Open

fix(lsp): detach workspace/diagnostic/refresh from notification handlers to prevent deadlock#25438
hamodywe wants to merge 1 commit into
oxc-project:mainfrom
hamodywe:fix/lsp-diagnostic-refresh-deadlock

Conversation

@hamodywe

@hamodywe hamodywe commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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.

@Sysix Sysix added the A-editor Area - Editor and Language Server label Aug 9, 2026
…ers to prevent deadlock

`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. Awaiting that round-trip inside the
notification handler that sent it pinned one of the transport's
concurrency slots (tower-lsp-server defaults to 4) for the entire
client round-trip. Four unanswered refreshes -- e.g. four rapid saves
of a watched config file in pull-diagnostic mode -- pinned every slot:
the server could no longer service the very diagnostic pulls the
client was waiting on before replying. Circular wait, no timeout, no
recovery until the process is killed; restarting the server re-wedges
it within minutes under the same editor behavior.

The refresh is advisory and nothing depends on its result beyond
logging a warning, so spawn it detached instead: the handler returns
its slot immediately and the server keeps answering requests while any
number of refresh replies are outstanding. Applies to all three call
sites (initialized, did_change_configuration, did_change_watched_files).

The regression test drives the real wire transport at its default
concurrency, holds 4 refresh replies open the way a busy client would,
and asserts the server still answers -- against the previous code it
times out exactly as reported.

Fixes oxc-project#24955
@hamodywe
hamodywe force-pushed the fix/lsp-diagnostic-refresh-deadlock branch from bf42082 to c5b627e Compare August 15, 2026 03:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-editor Area - Editor and Language Server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants