Skip to content

Commit ac79a17

Browse files
authored
Cancelling should invoke a single interrupt (#12106)
For #10496 Runing entire notebook and clicking cancel would result in kernel being interrupted n times (where n = number of cells). I.e. user clicks cancel once, but we end up interrupting multiple times.
1 parent bb812f5 commit ac79a17

File tree

4 files changed

+67
-22
lines changed

4 files changed

+67
-22
lines changed

src/client/datascience/interactive-ipynb/nativeEditorCommandListener.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Uri } from 'vscode';
99

1010
import { ICommandManager } from '../../common/application/types';
1111
import { IDisposableRegistry } from '../../common/types';
12+
import { traceError } from '../../logging';
1213
import { captureTelemetry } from '../../telemetry';
1314
import { CommandSource } from '../../testing/common/constants';
1415
import { Commands, Telemetry } from '../constants';
@@ -104,10 +105,10 @@ export class NativeEditorCommandListener implements IDataScienceCommandListener
104105
}
105106
}
106107

107-
private restartKernel() {
108+
private async restartKernel() {
108109
const activeEditor = this.provider.activeEditor;
109110
if (activeEditor) {
110-
activeEditor.restartKernel().ignoreErrors();
111+
await activeEditor.restartKernel().catch(traceError.bind('Failed to restart kernel'));
111112
}
112113
}
113114

src/client/datascience/notebook/executionService.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'
3636
*/
3737
@injectable()
3838
export class NotebookExecutionService implements INotebookExecutionService {
39-
private registeredIOPubListeners = new WeakSet<INotebook>();
39+
private readonly registeredIOPubListeners = new WeakSet<INotebook>();
4040
private _notebookProvider?: INotebookProvider;
41-
private pendingExecutionCancellations = new Map<string, CancellationTokenSource[]>();
41+
private readonly pendingExecutionCancellations = new Map<string, CancellationTokenSource[]>();
42+
private readonly tokensInterrupted = new WeakSet<CancellationToken>();
4243
private get notebookProvider(): INotebookProvider {
4344
this._notebookProvider =
4445
this._notebookProvider || this.serviceContainer.get<INotebookProvider>(INotebookProvider);
@@ -133,33 +134,21 @@ export class NotebookExecutionService implements INotebookExecutionService {
133134
const stopWatch = new StopWatch();
134135

135136
wrappedToken.onCancellationRequested(() => {
136-
// tslint:disable-next-line: no-suspicious-comment
137-
// TODO: Is this the right thing to do?
138-
// I think it is, as we have a stop button.
139-
// If we're busy executing, then interrupt the execution.
140137
if (deferred.completed) {
141138
return;
142139
}
143-
deferred.resolve();
144-
cell.metadata.runState = vscodeNotebookEnums.NotebookCellRunState.Idle;
145140

146141
// Interrupt kernel only if original cancellation was cancelled.
147-
if (token.isCancellationRequested) {
142+
// I.e. interrupt kernel only if user attempts to stop the execution by clicking stop button.
143+
if (token.isCancellationRequested && !this.tokensInterrupted.has(token)) {
144+
this.tokensInterrupted.add(token);
148145
this.commandManager.executeCommand(Commands.NotebookEditorInterruptKernel).then(noop, noop);
149146
}
150147
});
151148

152149
cell.metadata.runStartTime = new Date().getTime();
153150
cell.metadata.runState = vscodeNotebookEnums.NotebookCellRunState.Running;
154151

155-
if (!findMappedNotebookCellModel(cell, model.cells)) {
156-
// tslint:disable-next-line: no-suspicious-comment
157-
// TODO: Possible it was added as we didn't get to know about it.
158-
// We need to handle these.
159-
// Basically if there's a new cell, we need to first add it into our model,
160-
// Similarly we might want to handle deletions.
161-
throw new Error('Unable to find corresonding Cell in Model');
162-
}
163152
let subscription: Subscription | undefined;
164153
try {
165154
nb.clear(cell.uri.fsPath); // NOSONAR

src/test/datascience/notebook/helper.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { MARKDOWN_LANGUAGE, PYTHON_LANGUAGE } from '../../../client/common/const
1616
import { IDisposable } from '../../../client/common/types';
1717
import { noop, swallowExceptions } from '../../../client/common/utils/misc';
1818
import { NotebookContentProvider } from '../../../client/datascience/notebook/contentProvider';
19-
import { ICell, INotebookEditorProvider } from '../../../client/datascience/types';
19+
import { ICell, INotebookEditorProvider, INotebookProvider } from '../../../client/datascience/types';
2020
import { waitForCondition } from '../../common';
2121
import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants';
2222
import { closeActiveWindows, initialize } from '../../initialize';
@@ -162,11 +162,17 @@ export async function swallowSavingOfNotebooks() {
162162
sinon.stub(contentProvider, 'saveNotebookAs').callsFake(noop as any);
163163
}
164164

165+
export async function shutdownAllNotebooks() {
166+
const api = await initialize();
167+
const notebookProvider = api.serviceContainer.get<INotebookProvider>(INotebookProvider);
168+
await Promise.all(notebookProvider.activeNotebooks.map(async (item) => (await item).dispose()));
169+
}
165170
export async function closeNotebooksAndCleanUpAfterTests(disposables: IDisposable[] = []) {
166171
// We cannot close notebooks if there are any uncommitted changes (UI could hang with prompts etc).
167172
await commands.executeCommand('workbench.action.files.saveAll');
168173
await closeActiveWindows();
169174
disposeAllDisposables(disposables);
175+
await shutdownAllNotebooks();
170176
sinon.restore();
171177
}
172178

src/test/datascience/notebook/interrupRestart.ds.test.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
canRunTests,
2020
closeNotebooksAndCleanUpAfterTests,
2121
deleteAllCellsAndWait,
22+
disposeAllDisposables,
2223
insertPythonCellAndWait,
2324
startJupyter,
2425
swallowSavingOfNotebooks
@@ -39,6 +40,7 @@ suite('DataScience - VSCode Notebook - Restart/Interrupt/Cancel/Errors', functio
3940
let executionService: INotebookExecutionService;
4041
let vscEditor: VSCNotebookEditor;
4142
let vscodeNotebook: IVSCodeNotebook;
43+
const suiteDisposables: IDisposable[] = [];
4244
suiteSetup(async function () {
4345
this.timeout(15_000);
4446
api = await initialize();
@@ -60,7 +62,8 @@ suite('DataScience - VSCode Notebook - Restart/Interrupt/Cancel/Errors', functio
6062
vscEditor = vscodeNotebook.activeNotebookEditor!;
6163
});
6264
setup(deleteAllCellsAndWait);
63-
suiteTeardown(() => closeNotebooksAndCleanUpAfterTests(disposables));
65+
teardown(() => disposeAllDisposables(suiteDisposables));
66+
suiteTeardown(() => closeNotebooksAndCleanUpAfterTests(disposables.concat(suiteDisposables)));
6467

6568
test('Cancelling token will cancel cell execution (slow)', async () => {
6669
await insertPythonCellAndWait('import time\nfor i in range(10000):\n print(i)\n time.sleep(0.1)', 0);
@@ -122,6 +125,39 @@ suite('DataScience - VSCode Notebook - Restart/Interrupt/Cancel/Errors', functio
122125

123126
await waitForCondition(async () => assertVSCCellIsIdle(cell), 1_000, 'Execution not cancelled');
124127
});
128+
test('When running entire notebook, clicking VSCode Stop button should trigger a single interrupt, not one per cell', async () => {
129+
await insertPythonCellAndWait('import time\nfor i in range(10000):\n print(i)\n time.sleep(0.1)', 0);
130+
await insertPythonCellAndWait('import time\nfor i in range(10000):\n print(i)\n time.sleep(0.1)', 0);
131+
await insertPythonCellAndWait('import time\nfor i in range(10000):\n print(i)\n time.sleep(0.1)', 0);
132+
const cell1 = vscEditor.document.cells[0];
133+
const cell2 = vscEditor.document.cells[1];
134+
const cell3 = vscEditor.document.cells[2];
135+
136+
const interrupt = sinon.spy(editorProvider.activeEditor!, 'interruptKernel');
137+
suiteDisposables.push({ dispose: () => interrupt.restore() });
138+
139+
await commands.executeCommand('notebook.execute');
140+
141+
// Wait for cells to get busy.
142+
await waitForCondition(
143+
async () => assertVSCCellIsRunning(cell1) && assertVSCCellIsRunning(cell2) && assertVSCCellIsRunning(cell3),
144+
15_000,
145+
'Cells not being executed'
146+
);
147+
148+
// Cancel execution.
149+
await sleep(1_000);
150+
await commands.executeCommand('notebook.cancelExecution');
151+
152+
// Wait for ?s, and verify cells are not running.
153+
await waitForCondition(
154+
async () => assertVSCCellIsIdle(cell1) && assertVSCCellIsIdle(cell2) && assertVSCCellIsIdle(cell3),
155+
15_000,
156+
'Cells are still running'
157+
);
158+
159+
assert.equal(interrupt.callCount, 1, 'Interrupt should have been invoked only once');
160+
});
125161
test('Restarting kernel will cancel cell execution (slow)', async () => {
126162
await insertPythonCellAndWait('import time\nfor i in range(10000):\n print(i)\n time.sleep(0.1)', 0);
127163
const cell = vscEditor.document.cells[0];
@@ -136,8 +172,21 @@ suite('DataScience - VSCode Notebook - Restart/Interrupt/Cancel/Errors', functio
136172
assertVSCCellIsRunning(cell);
137173

138174
// Restart the kernel.
139-
await commands.executeCommand('python.datascience.notebookeditor.restartkernel');
175+
const restartPromise = commands.executeCommand('python.datascience.notebookeditor.restartkernel');
140176

141177
await waitForCondition(async () => assertVSCCellIsIdle(cell), 1_000, 'Execution not cancelled');
178+
179+
// Wait before we execute cells again.
180+
await restartPromise;
181+
await commands.executeCommand('notebook.execute');
182+
183+
// Wait for cell to get busy.
184+
await waitForCondition(async () => assertVSCCellIsRunning(cell), 15_000, 'Cell not being executed');
185+
186+
// Cleanup (don't leave them running).
187+
await commands.executeCommand('notebook.cancelExecution');
188+
189+
// Wait for ?s, and verify cells are not running.
190+
await waitForCondition(async () => assertVSCCellIsIdle(cell), 15_000, 'Cell is still running');
142191
});
143192
});

0 commit comments

Comments
 (0)