fix(lsp): stopped reaping clients with in-flight requests (#8390)
This commit is contained in:
committed by
can1357
parent
d62a986689
commit
9501b2299b
@@ -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);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user