Skip to content

Commit b997af6

Browse files
authored
fix(playgrounds): handle out of memory playground worker VSCODE-269 (#459)
1 parent ea6c408 commit b997af6

File tree

5 files changed

+106
-37
lines changed

5 files changed

+106
-37
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* This playground is used to test how the language server worker
3+
* handles an out of memory error in mongosh's running of a playground. VSCODE-269
4+
*/
5+
6+
use('test');
7+
8+
const mockDataArray = [];
9+
for(let i = 0; i < 50000; i++) {
10+
mockDataArray.push(Math.random() * 10000);
11+
}
12+
13+
const docs = [];
14+
for(let i = 0; i < 10000000; i++) {
15+
docs.push({
16+
mockData: [...mockDataArray],
17+
a: 'test 123',
18+
b: Math.ceil(Math.random() * 10000)
19+
});
20+
}
21+
22+
console.log('Should not show this message as the process should have run out of memory in the loop above.');

src/editors/playgroundController.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -332,21 +332,36 @@ export default class PlaygroundController {
332332

333333
this._statusView.showMessage('Getting results...');
334334

335-
// Send a request to the language server to execute scripts from a playground.
336-
const result: ShellExecuteAllResult =
337-
await this._languageServerController.executeAll({
338-
codeToEvaluate,
339-
connectionId,
340-
});
335+
try {
336+
// Send a request to the language server to execute scripts from a playground.
337+
const result: ShellExecuteAllResult =
338+
await this._languageServerController.executeAll({
339+
codeToEvaluate,
340+
connectionId,
341+
});
341342

342-
this._statusView.hideMessage();
343-
this._telemetryService.trackPlaygroundCodeExecuted(
344-
result,
345-
this._isPartialRun,
346-
result ? false : true
347-
);
343+
this._statusView.hideMessage();
344+
this._telemetryService.trackPlaygroundCodeExecuted(
345+
result,
346+
this._isPartialRun,
347+
result ? false : true
348+
);
348349

349-
return result;
350+
return result;
351+
} catch (err: any) {
352+
// We re-initialize the language server when we encounter an error.
353+
// This happens when the language server worker runs out of memory, can't be revitalized, and restarts.
354+
if (err?.code === -32097) {
355+
void vscode.window.showErrorMessage(
356+
'An error occurred when running the playground. This can occur when the playground runner runs out of memory.'
357+
);
358+
359+
await this._languageServerController.startLanguageServer();
360+
void this._connectToServiceProvider();
361+
}
362+
363+
throw err;
364+
}
350365
}
351366

352367
_getAllText(): string {

src/language/languageServerController.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,12 @@ export default class LanguageServerController {
8989

9090
async startLanguageServer(): Promise<void> {
9191
// Push the disposable client to the context's subscriptions so that the
92-
// client can be deactivated on extension deactivation
93-
this._context.subscriptions.push(this._client);
92+
// client can be deactivated on extension deactivation.
93+
if (!this._context.subscriptions.includes(this._client)) {
94+
this._context.subscriptions.push(this._client);
95+
}
9496

95-
// Subscribe on notifications from the server when the client is ready
97+
// Subscribe on notifications from the server when the client is ready.
9698
await this._client.sendRequest(
9799
ServerCommands.SET_EXTENSION_PATH,
98100
this._context.extensionPath

src/test/suite/editors/playgroundController.test.ts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,48 @@ suite('Playground Controller Test Suite', function () {
406406
});
407407
});
408408

409+
test('it shows an error message and restarts, and connects the language server when an error occurs in executeAll (out of memory can cause this)', async () => {
410+
const mockConnectionDisposedError = new Error(
411+
'Pending response rejected since connection got disposed'
412+
);
413+
(mockConnectionDisposedError as any).code = -32097;
414+
sinon
415+
.stub(mockLanguageServerController, 'executeAll')
416+
.rejects(mockConnectionDisposedError);
417+
418+
const stubStartLanguageServer = sinon
419+
.stub(mockLanguageServerController, 'startLanguageServer')
420+
.resolves();
421+
422+
const stubConnectToServiceProvider = sinon
423+
.stub(testPlaygroundController, '_connectToServiceProvider')
424+
.resolves();
425+
426+
const stubVSCodeErrorMessage = sinon
427+
.stub(vscode.window, 'showErrorMessage')
428+
.resolves(undefined);
429+
430+
try {
431+
await testPlaygroundController._evaluate('console.log("test");');
432+
433+
// It should have thrown in the above evaluation.
434+
expect(true).to.equal(false);
435+
} catch (err: any) {
436+
expect(err.message).to.equal(
437+
'Pending response rejected since connection got disposed'
438+
);
439+
expect(err.code).to.equal(-32097);
440+
}
441+
442+
expect(stubVSCodeErrorMessage.calledOnce).to.equal(true);
443+
expect(stubVSCodeErrorMessage.firstCall.args[0]).to.equal(
444+
'An error occurred when running the playground. This can occur when the playground runner runs out of memory.'
445+
);
446+
447+
expect(stubStartLanguageServer.calledOnce).to.equal(true);
448+
expect(stubConnectToServiceProvider.calledOnce).to.equal(true);
449+
});
450+
409451
test('playground controller loads the active editor on start', () => {
410452
sandbox.replaceGetter(
411453
vscode.window,
@@ -458,12 +500,9 @@ suite('Playground Controller Test Suite', function () {
458500
document: { getText: () => textFromEditor },
459501
} as vscode.TextEditor;
460502

461-
const fakeVscodeErrorMessage: any = sinon.fake();
462-
sinon.replace(
463-
vscode.window,
464-
'showErrorMessage',
465-
fakeVscodeErrorMessage
466-
);
503+
const fakeVscodeErrorMessage = sinon
504+
.stub(vscode.window, 'showErrorMessage')
505+
.resolves(undefined);
467506

468507
playgroundControllerTest._selectedText = '{ name: qwerty }';
469508
playgroundControllerTest._codeActionProvider.selection = selection;
@@ -474,7 +513,9 @@ suite('Playground Controller Test Suite', function () {
474513

475514
const expectedMessage =
476515
"Unable to export to csharp language: Symbol 'qwerty' is undefined";
477-
expect(fakeVscodeErrorMessage.firstArg).to.be.equal(expectedMessage);
516+
expect(fakeVscodeErrorMessage.firstCall.args[0]).to.equal(
517+
expectedMessage
518+
);
478519
});
479520
});
480521
});

src/test/suite/explorer/playgroundsExplorer.test.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,13 @@ suite('Playgrounds Controller Test Suite', function () {
5454
try {
5555
const children = await treeController.getPlaygrounds(rootUri);
5656

57-
assert.strictEqual(
58-
Object.keys(children).length,
59-
5,
60-
`Tree playgrounds should have 5 child, found ${children.length}`
61-
);
57+
assert.strictEqual(Object.keys(children).length, 6);
6258

6359
const playgrounds = Object.values(children).filter(
6460
(item: any) => item.label && item.label.split('.').pop() === 'mongodb'
6561
);
6662

67-
assert.strictEqual(
68-
Object.keys(playgrounds).length,
69-
5,
70-
`Tree playgrounds should have 5 playgrounds with mongodb extension, found ${children.length}`
71-
);
63+
assert.strictEqual(Object.keys(playgrounds).length, 6);
7264
} catch (error) {
7365
assert(false, error as Error);
7466
}
@@ -92,10 +84,7 @@ suite('Playgrounds Controller Test Suite', function () {
9284
const treeController = new PlaygroundsTree();
9385
const children = await treeController.getPlaygrounds(rootUri);
9486

95-
assert(
96-
Object.keys(children).length === 3,
97-
`Tree playgrounds should have 3 child, found ${children.length}`
98-
);
87+
assert.strictEqual(Object.keys(children).length, 3);
9988
} catch (error) {
10089
assert(false, error as Error);
10190
}

0 commit comments

Comments
 (0)