fix(lsp): stopped reaping clients with in-flight requests (#8390)

This commit is contained in:
Muhammad Mustaqeem
2026-08-14 11:32:53 +05:00
committed by can1357
parent d62a986689
commit 9501b2299b
+28 -2
View File
@@ -62,13 +62,32 @@ export function setIdleTimeout(ms: number | null | undefined): void {
}
}
/**
* Whether a client may be reaped by the idle checker.
*
* A client with in-flight requests is *busy*, never idle. `lastActivity` is
* stamped when a request is written, not while it is outstanding, so a single
* request that runs longer than the idle timeout used to look like silence:
* the checker tore the client down mid-flight and `shutdownClientInstance`
* rejected the caller's still-pending promise with "LSP client shutdown"
* (issue #8390). Requests that settle refresh `lastActivity`, so a client
* becomes eligible again only after the final one lands and the full idle
* window then elapses.
*
* Exported for tests; the idle checker is the only production caller.
*/
export function isIdleClient(client: LspClient, now: number, timeoutMs: number): boolean {
if (client.pendingRequests.size > 0) return false;
return now - client.lastActivity > timeoutMs;
}
function startIdleChecker(): void {
if (idleCheckInterval) return;
idleCheckInterval = setInterval(() => {
if (!idleTimeoutMs) return;
const now = Date.now();
for (const [key, client] of Array.from(clients.entries())) {
if (now - client.lastActivity > idleTimeoutMs) {
if (isIdleClient(client, now, idleTimeoutMs)) {
void shutdownClient(key);
}
}
@@ -1462,15 +1481,22 @@ export async function sendRequest(
}
}
// Register pending request with timeout wrapper
// Register pending request with timeout wrapper.
// Settling stamps `lastActivity`: the idle window must be measured from when
// the exchange finished, not from when it started. Without this a request
// that outlives the timeout would leave the client instantly reapable the
// moment it lands, so the next idle sweep would kill a server that had just
// answered (issue #8390).
client.pendingRequests.set(id, {
resolve: result => {
if (timeout) clearTimeout(timeout);
client.lastActivity = Date.now();
cleanup();
resolve(result);
},
reject: err => {
if (timeout) clearTimeout(timeout);
client.lastActivity = Date.now();
cleanup();
reject(err);
},