Skip to content

Commit 48c6b08

Browse files
committed
[release] src/goLanguageServer: timeout LanguageClient.stop call
LanguageClient.stop hangs indefinitely if the language server fails to respond to the `shutdown` request. For example, in go.dev/issues/52543 we observed `gopls` crashes during shutdown. Implement a timeout from our side. (2sec) Caveat: If gopls is still active but fails to respond within 2sec, it's possible that we may end up having multiple gopls instances briefly until the previous gopls completes the shutdown process. For #1896 For #2222 Change-Id: Idbcfd3ee5f94fd3fd8dcafa228c6f03f5e14b905 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/402834 Run-TryBot: Hyang-Ah Hana Kim <[email protected]> TryBot-Result: kokoro <[email protected]> Reviewed-by: Suzy Mueller <[email protected]> Reviewed-by: Jamal Carvalho <[email protected]> (cherry picked from commit 9227019) Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/403414
1 parent d76c22f commit 48c6b08

File tree

1 file changed

+35
-7
lines changed

1 file changed

+35
-7
lines changed

src/language/goLanguageServer.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -358,19 +358,18 @@ export const flushGoplsOptOutConfig = (cfg: GoplsOptOutConfig, workspace: boolea
358358
async function startLanguageServer(ctx: vscode.ExtensionContext, config: LanguageServerConfig): Promise<boolean> {
359359
// If the client has already been started, make sure to clear existing
360360
// diagnostics and stop it.
361+
let cleanStop = true;
361362
if (languageClient) {
362-
if (languageClient.diagnostics) {
363-
languageClient.diagnostics.clear();
364-
}
365-
await languageClient.stop();
363+
cleanStop = await stopLanguageClient(languageClient);
366364
if (languageServerDisposable) {
367365
languageServerDisposable.dispose();
368366
}
369367
}
370368

371-
// Check if we should recreate the language client. This may be necessary
372-
// if the user has changed settings in their config.
373-
if (!deepEqual(latestConfig, config)) {
369+
// Check if we should recreate the language client.
370+
// This may be necessary if the user has changed settings
371+
// in their config, or previous session wasn't stopped cleanly.
372+
if (!cleanStop || !deepEqual(latestConfig, config)) {
374373
// Track the latest config used to start the language server,
375374
// and rebuild the language client.
376375
latestConfig = config;
@@ -411,6 +410,35 @@ async function startLanguageServer(ctx: vscode.ExtensionContext, config: Languag
411410
return true;
412411
}
413412

413+
const race = function (promise: Promise<unknown>, timeoutInMilliseconds: number) {
414+
let token: NodeJS.Timeout;
415+
const timeout = new Promise((resolve, reject) => {
416+
token = setTimeout(() => reject('timeout'), timeoutInMilliseconds);
417+
});
418+
return Promise.race([promise, timeout]).then(() => clearTimeout(token));
419+
};
420+
421+
// exported for testing.
422+
export async function stopLanguageClient(c: LanguageClient): Promise<boolean> {
423+
if (!c) return false;
424+
425+
if (c.diagnostics) {
426+
c.diagnostics.clear();
427+
}
428+
// LanguageClient.stop may hang if the language server
429+
// crashes during shutdown before responding to the
430+
// shutdown request. Enforce client-side timeout.
431+
// TODO(hyangah): replace with the new LSP client API that supports timeout
432+
// and remove this.
433+
try {
434+
await race(c.stop(), 2000);
435+
} catch (e) {
436+
c.outputChannel?.appendLine(`Failed to stop client: ${e}`);
437+
return false;
438+
}
439+
return true;
440+
}
441+
414442
function toServerInfo(res?: InitializeResult): ServerInfo | undefined {
415443
if (!res) return undefined;
416444

0 commit comments

Comments
 (0)