feat(lsp): idle-timeout shutdown for LSP servers - #809
Conversation
Shut down gopls/tsserver subprocesses after 15 minutes of inactivity, transparently restart on next use via getOrCreateClient cache-miss path. Implementation: - shutdownIdleClientsIn(): pure function that sweeps a client map, skipping clients with pending requests or active progress tokens. Fully synchronous check-delete-kill — no event-loop interleaving with getOrCreateClient's synchronous cache-hit path. - shutdownIdleClients(): thin wrapper passing module-level maps. - 60s setInterval sweep wired in session_start, cleared on shutdown. Duplicate-safe (clears previous timer before starting new one). Tests: 8 unit tests for shutdownIdleClientsIn (threshold, guards, ordering, multi-client, empty map) + 5 extension tests for timer lifecycle (start, threshold value, no-servers, clear, no-duplicate).
|
📚 No documentation changes were needed for this PR. |
Kimchi Code Review
Summary📊 Review Score: 90/100 (overall code quality — 0 lowest, 100 highest) 🧪 Tests: yes — Comprehensive tests cover the extension-level timer lifecycle (start, stop, duplicate prevention, no-servers cleanup, threshold passing) and the client sweep logic (idle detection, pending request/progress token preservation, lock map cleanup, shutdown-before-kill ordering, error resilience, and initialize-request protection). 📝 Found 1 issue(s). See inline comments for details. What to expectKimchi will analyze the changes in this pull request and post:
The review typically completes within a few minutes. This comment will be updated once the review is ready. Interact with Kimchi
ConfigurationReviews are configured by your organization admin. Powered by Kimchi — AI-powered code review by CAST AI |
There was a problem hiding this comment.
📊 Review Score: 86/100 (overall code quality — 0 lowest, 100 highest)
⏱️ Estimated effort to review: 2/5 (1 = trivial, 5 = very complex)
🧪 Tests: yes — The PR adds targeted tests for both the idle sweep timer lifecycle in src/extensions/lsp.test.ts and the idle-shutdown logic in the new src/extensions/lsp/client.test.ts. Coverage includes threshold handling, pending/progress-token protection, shutdown request ordering, duplicate timer prevention, and cleanup on shutdown.
📝 Found 4 issue(s). See inline comments for details.
b7f071d to
56b0fe4
Compare
|
@getkimchi review |
|
🔄 Starting review on |
There was a problem hiding this comment.
📊 Review Score: 88/100 (overall code quality — 0 lowest, 100 highest)
⏱️ Estimated effort to review: 2/5 (1 = trivial, 5 = very complex)
🧪 Tests: yes — Good coverage for the new behavior. src/extensions/lsp.test.ts verifies timer lifecycle (start, threshold passing, no-servers skip, shutdown cleanup, duplicate prevention, and sweep clearing). src/extensions/lsp/client.test.ts adds thorough unit tests for shutdownIdleClientsIn including idle/active clients, pending requests, progress tokens, lock-map cleanup, mid-initialize protection, ordering of shutdown write vs kill, and kill errors.
📝 Found 2 issue(s). See inline comments for details.
56b0fe4 to
89468e4
Compare
|
@getkimchi review |
|
🔄 Starting review on |
There was a problem hiding this comment.
📊 Review Score: 90/100 (overall code quality — 0 lowest, 100 highest)
⏱️ Estimated effort to review: 3/5 (1 = trivial, 5 = very complex)
🧪 Tests: yes — Comprehensive tests cover the extension-level timer lifecycle (start, stop, duplicate prevention, no-servers cleanup, threshold passing) and the client sweep logic (idle detection, pending request/progress token preservation, lock map cleanup, shutdown-before-kill ordering, error resilience, and initialize-request protection).
📝 Found 1 issue(s). See inline comments for details.
- Move timer clear above the no-servers early return in session_start so a prior session's interval doesn't leak when the next session detects no LSP servers. - Wrap sendRequest/proc.kill in try/catch inside shutdownIdleClientsIn to prevent an uncaughtException in the setInterval callback if the process is already dead. - Add regression tests: stale sweep on no-servers restart, mid-initialize client preserved, proc.kill throwing on dead process. Co-Authored-By: Kimchi <noreply@kimchi.dev>
89468e4 to
afcbc6f
Compare
| // controller is combined with ctx.signal so user/session aborts also unwind | ||
| // the wait, but we never abort ctx.signal ourselves. | ||
| let pendingRefresh: { abort: AbortController } | undefined | ||
| let idleSweepTimer: ReturnType<typeof setInterval> | null = null |
There was a problem hiding this comment.
There is a nodejs.Timeout type that can be used for this
| if (typeof idleSweepTimer === "object" && "unref" in idleSweepTimer) { | ||
| idleSweepTimer.unref() |
There was a problem hiding this comment.
No need for these additional checks, this is synchronous execution, timeout object will always be available at this point
| if (typeof idleSweepTimer === "object" && "unref" in idleSweepTimer) { | |
| idleSweepTimer.unref() | |
| idleSweepTimer.unref() |
| } | ||
|
|
||
| /** | ||
| * Shut down LSP clients that have been idle for longer than `thresholdMs`. |
There was a problem hiding this comment.
I wonder if the same could be done for regular commands? I've had vitest freezing up the client sometimes
There was a problem hiding this comment.
If it works well, it is possible
Address PR feedback from @vmizg — use NodeJS.Timeout instead of ReturnType<typeof setInterval>. This also lets us drop the runtime guard around .unref() since the method is declared on the type.
Merge origin/master into feat/lsp-idle-shutdown. Conflict was in the session_start handler of src/extensions/lsp.ts: - Master added degraded-state detection (detectMissingCandidates, warned flag, before_agent_start one-time warning) and restructured the status-bar logic. - Our branch added the idle sweep timer (start on session_start, clear on session_shutdown). Resolution: kept both features. The idle sweep timer start was moved after the activeServers.length === 0 early return so it only starts when servers are actually active. Timer clearing stays before the early return to prevent leaks from prior sessions.
|
@vmizg updated from code review + resolved merge conflicts, PTAL |
Linked issue
Closes #808
What does this PR do?
Introduces a background sweeper for idle servers. If a server is idle for too long (15 minute hard threshold now), it is stopped and will have to be started again. This is a tradeoff between latency to restart a server sometimes (and let it do its processing) vs idle resource usage.
How it works
A 60-second setInterval sweep checks each live LSP client against three conditions:
Idle clients get a best-effort LSP shutdown request followed by proc.kill(). Map entries are deleted synchronously before kill so getOrCreateClient never returns a dying client — JS single-threading guarantees no interleaving with the
synchronous cache-hit path.
The next LSP tool call transparently spawns a fresh server.
Changes
process can't crash the interval callback.
and proc.kill throwing.
Checklist
pnpm run test) (some tests seem to fail but they are in files unrelated to change)pnpm run check)