Skip to content

Commit 0aeab66

Browse files
committed
[release] src/goLanguageServer: protect language restart with mutex
We have used the languageServerStartInProgress flag to prevent issuing another language start request while the previous language start up process is in progress and is blocked. This prevented the race of multiple language start calls. However, this can cause important language client restart request to be skipped. Consider this scenario: - user modifies the setting to enable gopls and that triggers a call to startLanguageServerWithFallback. While this is waiting on startLanguageServer that will run with cfg.enabled = true. - user modifies the stting to disable gopls, and that triggers another call to startLanguageServerWithFallback. - the second startLanguageServerWithFallback will skip startLanguageServer because languageServerStartInProgress is true. - As a result, we will fail to disable the language server. This change fixes the issue by using a new Mutex to protect startLanguageServer. With the change, the second call won't skip startLanguageServer, but will be resumed when the previous startLanguageServer call is finished. This change also fixes the bug in src/goStatus that produced incorrect language server status icon when language server is disabled. Fixes #1132 Change-Id: I4435d41b843032ff8f675ea95aac002d9ba79b4b Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/288352 Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com> TryBot-Result: kokoro <noreply+kokoro@google.com> Trust: Hyang-Ah Hana Kim <hyangah@gmail.com> Reviewed-by: Rebecca Stambler <rstambler@golang.org> (cherry picked from commit 1ad5c33) Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/289234
1 parent 9b53776 commit 0aeab66

File tree

4 files changed

+118
-23
lines changed

4 files changed

+118
-23
lines changed

src/goLanguageServer.ts

+17-21
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import {
6464
getWorkspaceFolderPath,
6565
removeDuplicateDiagnostics
6666
} from './util';
67+
import { Mutex } from './utils/mutex';
6768
import { getToolFromToolPath } from './utils/pathUtils';
6869

6970
export interface LanguageServerConfig {
@@ -88,9 +89,9 @@ let languageServerDisposable: vscode.Disposable;
8889
let latestConfig: LanguageServerConfig;
8990
export let serverOutputChannel: vscode.OutputChannel;
9091
export let languageServerIsRunning = false;
91-
// TODO: combine languageServerIsRunning & languageServerStartInProgress
92-
// as one languageServerStatus variable.
93-
let languageServerStartInProgress = false;
92+
93+
const languageServerStartMutex = new Mutex();
94+
9495
let serverTraceChannel: vscode.OutputChannel;
9596
let crashCount = 0;
9697

@@ -121,11 +122,6 @@ export async function startLanguageServerWithFallback(ctx: vscode.ExtensionConte
121122
}
122123
}
123124

124-
if (!activation && languageServerStartInProgress) {
125-
console.log('language server restart is already in progress...');
126-
return;
127-
}
128-
129125
const goConfig = getGoConfig();
130126
const cfg = buildLanguageServerConfig(goConfig);
131127

@@ -145,21 +141,21 @@ export async function startLanguageServerWithFallback(ctx: vscode.ExtensionConte
145141
}
146142
}
147143
}
144+
const unlock = await languageServerStartMutex.lock();
145+
try {
146+
const started = await startLanguageServer(ctx, cfg);
148147

149-
languageServerStartInProgress = true;
150-
151-
const started = await startLanguageServer(ctx, cfg);
152-
153-
// If the server has been disabled, or failed to start,
154-
// fall back to the default providers, while making sure not to
155-
// re-register any providers.
156-
if (!started && defaultLanguageProviders.length === 0) {
157-
registerDefaultProviders(ctx);
148+
// If the server has been disabled, or failed to start,
149+
// fall back to the default providers, while making sure not to
150+
// re-register any providers.
151+
if (!started && defaultLanguageProviders.length === 0) {
152+
registerDefaultProviders(ctx);
153+
}
154+
languageServerIsRunning = started;
155+
updateLanguageServerIconGoStatusBar(started, goConfig['useLanguageServer'] === true);
156+
} finally {
157+
unlock();
158158
}
159-
160-
languageServerIsRunning = started;
161-
updateLanguageServerIconGoStatusBar(started, goConfig['useLanguageServer'] === true);
162-
languageServerStartInProgress = false;
163159
}
164160

165161
// scheduleGoplsSuggestions sets timeouts for the various gopls-specific

src/goStatus.ts

-2
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,8 @@ export function updateLanguageServerIconGoStatusBar(started: boolean, enabled: b
129129
let text = goEnvStatusbarItem.text;
130130
let icon = '';
131131
if (text.endsWith(languageServerIcon)) {
132-
icon = languageServerIcon;
133132
text = text.substring(0, text.length - languageServerIcon.length);
134133
} else if (text.endsWith(languageServerErrorIcon)) {
135-
icon = languageServerErrorIcon;
136134
text = text.substring(0, text.length - languageServerErrorIcon.length);
137135
}
138136

src/utils/mutex.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*---------------------------------------------------------
2+
* Copyright 2021 The Go Authors. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE in the project root for license information.
4+
*--------------------------------------------------------*/
5+
6+
'use strict';
7+
8+
/* Mutex provides mutex feature by building a promise chain.
9+
10+
const m = new Mutex();
11+
12+
const unlock = await m.lock();
13+
try {
14+
// critical section
15+
} finally {
16+
unlock();
17+
}
18+
*/
19+
export class Mutex {
20+
private mutex = Promise.resolve();
21+
22+
public lock(): PromiseLike<() => void> {
23+
// Based on https://spin.atomicobject.com/2018/09/10/javascript-concurrency/
24+
25+
let x: (unlock: () => void) => void;
26+
27+
// add to the promise chain of this mutex.
28+
// When all the prior promises in the chain are resolved,
29+
// x, which will be the resolve callback of promise B,
30+
// will run and cause to unblock the waiter of promise B.
31+
this.mutex = this.mutex.then(() => {
32+
return new Promise(x); // promise A
33+
});
34+
35+
return new Promise((resolve) => { // promise B
36+
x = resolve;
37+
});
38+
// the returned Promise will resolve when all the previous
39+
// promises chained in this.mutex resolve.
40+
}
41+
}

test/unit/mutex.test.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*---------------------------------------------------------
2+
* Copyright 2021 The Go Authors. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE in the project root for license information.
4+
*--------------------------------------------------------*/
5+
6+
import * as assert from 'assert';
7+
import { Mutex } from '../../src/utils/mutex';
8+
9+
suite('Mutex Tests', () => {
10+
test('works for basic concurrent access', async () => {
11+
const m = new Mutex();
12+
13+
let cnt = 0;
14+
const worker = async (delay: number, count: number) => {
15+
for (let i = 0; i < count; i++) {
16+
const unlock = await m.lock();
17+
try {
18+
const cntCopy = cnt;
19+
await sleep(delay);
20+
cnt = cntCopy + 1;
21+
} finally {
22+
unlock();
23+
}
24+
}
25+
};
26+
27+
await Promise.all([worker(3, 5), worker(1, 10)]);
28+
assert.strictEqual(cnt, 15);
29+
});
30+
31+
test('works when lock holders throw errors', async () => {
32+
const m = new Mutex();
33+
34+
let cnt = 0;
35+
const worker = async (delay: number) => {
36+
const unlock = await m.lock();
37+
try {
38+
const cntCopy = cnt;
39+
await sleep(delay);
40+
cnt = cntCopy + 1;
41+
throw new Error('ooops');
42+
} finally {
43+
unlock();
44+
}
45+
};
46+
47+
const safeWorker = async (delay: number) => {
48+
try {
49+
await worker(delay);
50+
} catch (e) {
51+
// swallow the exception
52+
}
53+
};
54+
55+
await Promise.all([safeWorker(3), safeWorker(2), safeWorker(1), safeWorker(0)]);
56+
assert.strictEqual(cnt, 4);
57+
});
58+
});
59+
60+
function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); }

0 commit comments

Comments
 (0)