1
0
mirror of https://github.com/helix-editor/helix synced 2026-07-21 21:24:55 +02:00
Files
Inian Vasanth ad6a1b1bdf fix(lsp): break shutdown deadlock and use fire-and-forget exit (#15553)
* fix(lsp): break shutdown deadlock and use fire-and-forget exit

Two related problems caused `Error: Timed out waiting for language
servers to shutdown`, most visibly with gopls on large Go monorepos.

**Problem 1 — deadlock**

When helix sends `shutdown`, some servers (gopls, vscode-languageserver-
node family) send `client/registerCapability` and `workspace/configuration`
requests *back* to helix before responding. The application event loop is
blocked at `close_language_servers().await` and cannot consume these
requests. The server waits for helix to respond; helix waits for the
server's shutdown response. Classic deadlock broken only by the 3-second
timeout.

Fix: add `shutdown_requested: AtomicBool` and an `inject_tx` channel to
`Transport`. When the flag is set and a server-to-client `MethodCall`
arrives, the `recv` task immediately sends a null-success response via
`inject_tx` rather than forwarding to the dead application queue. The flag
is set *before* writing shutdown to stdin to close the race window. Null
success is used rather than an error because `application.rs` already
documents that an error response to `registerCapability` causes
vscode-languageserver-node servers to abort rather than complete the
shutdown handshake.

**Problem 2 — blocking design**

Even with the deadlock broken, `force_shutdown` blocked on `shutdown().await`
(up to 20 s per-request timeout) while the outer `join_all` had only a 3 s
budget. On large monorepos gopls flushes ~1 300 `window/logMessage`
notifications before responding, pushing the total past 3 s.

The deeper issue is that blocking the application task during shutdown
prevents it from handling any server-to-client traffic, making the system
fragile against any slow or chatty server.

Fix: make `force_shutdown` a plain (non-async) fire-and-forget function
that queues `shutdown` + `exit` in the send channel and returns immediately.
The response channel receiver is dropped on purpose; any late response is
discarded at debug level. `close_language_servers` is simplified to a
synchronous loop + a 50 ms sleep, which gives the send tasks one scheduling
quantum to flush the queued bytes before the runtime drops. Servers that do
not receive the bytes before process exit will see stdin EOF and exit cleanly.

This matches the approach already used for uninitialized servers since #7449,
and mirrors Neovim's default `exit_timeout = false` behaviour.

Fixes #14724
Fixes #14679
See also #5117, #5174, #4551, #11542

* fix linter issues

* transfer child to background thread on shutdown

* lsp: Immediately terminate on quit, wait until servers have been notified

* clippy lint

---------

Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
2026-06-20 14:38:42 +09:00
..