Skip to content

Commit 91737c6

Browse files
authored
Potential fixes for cancel and session timeout on shutdown (microsoft#4845)
For #4827 <!-- If an item below does not apply to you, then go ahead and check it off as "done" and strikethrough the text, e.g.: - [x] ~Has unit tests & system/integration tests~ --> - [x] Pull request represents a single change (i.e. not fixing disparate/unrelated things in a single PR) - [x] Title summarizes what is changing - [x] Has a [news entry](https://github.com/Microsoft/vscode-python/tree/master/news) file (remember to thank yourself!) - [ ] Has sufficient logging. - [ ] Has telemetry for enhancements. - [x] Unit tests & system/integration tests are added/updated - [ ] [Test plan](https://github.com/Microsoft/vscode-python/blob/master/.github/test_plan.md) is updated as appropriate - [ ] [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/master/package-lock.json) has been regenerated by running `npm install` (if dependencies have changed) - [ ] The wiki is updated with any design decisions/details.
1 parent 17f9647 commit 91737c6

4 files changed

Lines changed: 30 additions & 14 deletions

File tree

news/3 Code Health/4827.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Handle done on all jupyter requests to make sure an unhandle exception isn't passed on shutdown.

src/client/common/utils/async.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,16 @@ export function createDeferredFromPromise<T>(promise: Promise<T>): Deferred<T> {
8484
.catch(deferred.reject.bind(deferred));
8585
return deferred;
8686
}
87+
export function callWithTimeout<T>(func: () => T, timeoutMS: number) : Promise<T> {
88+
return new Promise<T>((resolve, reject) => {
89+
setTimeout(() => {
90+
reject(new Error('Timed out'));
91+
}, timeoutMS);
92+
try {
93+
const result = func();
94+
resolve(result);
95+
} catch (e) {
96+
reject(e);
97+
}
98+
});
99+
}

src/client/datascience/jupyter/jupyterSession.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { Event, EventEmitter } from 'vscode';
1616
import { CancellationToken } from 'vscode-jsonrpc';
1717

1818
import { Cancellation } from '../../common/cancellation';
19-
import { sleep } from '../../common/utils/async';
19+
import { callWithTimeout, sleep } from '../../common/utils/async';
2020
import * as localize from '../../common/utils/localize';
2121
import { noop } from '../../common/utils/misc';
2222
import { IConnection, IJupyterKernelSpec, IJupyterSession } from '../types';
@@ -180,22 +180,23 @@ export class JupyterSession implements IJupyterSession {
180180
this.statusHandler = undefined;
181181
}
182182
if (this.session) {
183-
// Shutdown may fail if the process has been killed
184-
await Promise.race([this.session.shutdown(), sleep(100)]);
183+
try {
184+
// Shutdown may fail if the process has been killed
185+
await Promise.race([this.session.shutdown(), sleep(100)]);
186+
} catch {
187+
noop();
188+
}
189+
// Dispose may not return. Wrap in a promise instead. Kernel futures can die if
190+
// process is already dead.
185191
if (this.session) {
186-
this.session.dispose();
192+
await callWithTimeout(this.session.dispose.bind(this.session), 100);
187193
}
188194
}
189195
if (this.sessionManager) {
190-
this.sessionManager.dispose();
196+
await callWithTimeout(this.sessionManager.dispose.bind(this.sessionManager), 100);
191197
}
192198
} catch {
193-
if (this.session) {
194-
this.session.dispose();
195-
}
196-
if (this.sessionManager) {
197-
this.sessionManager.dispose();
198-
}
199+
noop();
199200
}
200201
this.session = undefined;
201202
this.sessionManager = undefined;

src/test/datascience/notebook.functional.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -427,17 +427,18 @@ suite('Jupyter notebook tests', () => {
427427

428428
async function testCancelableCall<T>(method: (t: CancellationToken) => Promise<T>, messageFormat: string, timeout: number): Promise<boolean> {
429429
const tokenSource = new TaggedCancellationTokenSource(messageFormat.format(timeout.toString()));
430+
let canceled = false;
430431
const disp = setTimeout((s) => {
432+
canceled = true;
431433
tokenSource.cancel();
432434
}, timeout, tokenSource.tag);
433435

434436
try {
435437
// tslint:disable-next-line:no-string-literal
436438
(tokenSource.token as any)['tag'] = messageFormat.format(timeout.toString());
437439
await method(tokenSource.token);
438-
// We might get here before the cancel finishes. Wait for a timeout and then check cancel fired.
439-
await sleep(timeout);
440-
assert.ok(!tokenSource.token.isCancellationRequested, messageFormat.format(timeout.toString()));
440+
// We might get here before the cancel finishes
441+
assert.ok(!canceled, messageFormat.format(timeout.toString()));
441442
} catch (exc) {
442443
// This should happen. This means it was canceled.
443444
assert.ok(exc instanceof CancellationError, `Non cancellation error found : ${exc.stack}`);

0 commit comments

Comments
 (0)