Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle situation when remote connection is lost #11086

Merged
merged 3 commits into from
Aug 10, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions news/2 Fixes/10568.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Handle situations when the connection to a remote Kernel is lost after executing a cell.
This also fixes the issue [Unable to interrupt a kernel when the remote kernel connection is lost #9828](https://github.com/microsoft/vscode-jupyter/issues/9828)
2 changes: 2 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@
"DataScienceSurveyBanner.bannerLabelYes": "Yes, take survey now",
"DataScienceSurveyBanner.bannerLabelNo": "No, thanks",
"InteractiveShiftEnterBanner.bannerMessage": "Would you like to run code in the 'Interactive' window (an IPython console) for 'shift-enter'? Select 'No' to continue to run code in the Python Terminal. This can be changed later in settings.",
"DataScience.automaticallyReconnectingToAKernelProgressMessage": "Reconnecting to the kernel {0}",
"DataScience.restartingKernelStatus": {
"message": "Restarting Kernel {0}",
"comment": ["{Locked='Kernel'}"]
Expand Down Expand Up @@ -846,6 +847,7 @@
"message": "The kernel '{0}' died and is being automatically restarted by Jupyter. Click [here](https://aka.ms/vscodeJupyterKernelCrash) for more info. View Jupyter [log](command:jupyter.viewOutput) for further details.",
"comment": ["{Locked='Kernel'}", "{Locked='command:jupyter.viewOutput'}", "{Locked='vscodeJupyterKernelCrash'}"]
},
"DataScience.kernelDisconnected": "Unable to connect to the kernel, please verify the Jupyter Server connection. View Jupyter [log](command:jupyter.viewOutput) for further details.",
"DataScience.kernelCrashedDueToCodeInCurrentOrPreviousCell": {
"message": "The Kernel crashed while executing code in the the current cell or a previous cell. Please review the code in the cell(s) to identify a possible cause of the failure. Click <a href='https://aka.ms/vscodeJupyterKernelCrash'>here</a> for more info. View Jupyter [log](command:jupyter.viewOutput) for further details.",
"comment": ["{Locked='Kernel'}", "{Locked='command:jupyter.viewOutput'}", "{Locked='vscodeJupyterKernelCrash'}"]
Expand Down
12 changes: 12 additions & 0 deletions src/kernels/common/baseJupyterSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,14 @@ export abstract class BaseJupyterSession implements IBaseKernelConnectionSession

// Rewire our status changed event.
newSession.statusChanged.connect(this.statusHandler);
newSession.kernel.connectionStatusChanged.connect(this.onKernelConnectionStatusHandler, this);
}
traceInfo('Started new restart session');
if (oldStatusHandler && oldSession) {
oldSession.statusChanged.disconnect(oldStatusHandler);
if (oldSession.kernel) {
oldSession.kernel.connectionStatusChanged.disconnect(this.onKernelConnectionStatusHandler, this);
}
}
this.shutdownSession(oldSession, undefined, false).ignoreErrors();
}
Expand Down Expand Up @@ -410,6 +414,7 @@ export abstract class BaseJupyterSession implements IBaseKernelConnectionSession
}
if (this.statusHandler) {
oldSession.statusChanged.disconnect(this.statusHandler);
oldSession.kernel?.connectionStatusChanged.disconnect(this.onKernelConnectionStatusHandler, this);
}
}
this._session = session;
Expand All @@ -420,6 +425,7 @@ export abstract class BaseJupyterSession implements IBaseKernelConnectionSession

// Listen for session status changes
session.statusChanged.connect(this.statusHandler);
session.kernel?.connectionStatusChanged.connect(this.onKernelConnectionStatusHandler, this);
if (session.kernelSocketInformation.socket?.onAnyMessage) {
// These messages are sent directly to the kernel bypassing the Jupyter lab npm libraries.
// As a result, we don't get any notification that messages were sent (on the anymessage signal).
Expand Down Expand Up @@ -568,6 +574,12 @@ export abstract class BaseJupyterSession implements IBaseKernelConnectionSession
return 'unknown';
}

private onKernelConnectionStatusHandler(_: unknown, kernelConnection: Kernel.ConnectionStatus) {
if (kernelConnection === 'disconnected') {
const status = this.getServerStatus();
this.onStatusChangedEvent.fire(status);
}
}
private onStatusChanged(_s: Session.ISessionConnection) {
const status = this.getServerStatus();
traceInfoIfCI(`Server Status = ${status}`);
Expand Down
111 changes: 111 additions & 0 deletions src/kernels/kernelAutoReConnectFailedMonitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import type { Kernel } from '@jupyterlab/services';
import { inject, injectable } from 'inversify';
import { NotebookCell } from 'vscode';
import { IExtensionSyncActivationService } from '../platform/activation/types';
import { IDisposableRegistry } from '../platform/common/types';
import { DataScience } from '../platform/common/utils/localize';
import { Telemetry } from '../telemetry';
import { endCellAndDisplayErrorsInCell } from './execution/helpers';
import { getDisplayNameOrNameOfKernelConnection } from './helpers';
import { sendKernelTelemetryEvent } from './telemetry/sendKernelTelemetryEvent';
import { IKernel, IKernelProvider, isLocalConnection } from './types';

/**
* In the case of Jupyter kernels, when a kernel dies Jupyter will automatically restart that kernel.
* In such a case we need to display a little progress indicator so user is aware of the fact that the kernel is restarting.
*/
@injectable()
export class kernelAutoReConnectFailedMonitor implements IExtensionSyncActivationService {
DonJayamanne marked this conversation as resolved.
Show resolved Hide resolved
private kernelsStartedSuccessfully = new WeakSet<IKernel>();
private kernelConnectionToKernelMapping = new WeakMap<Kernel.IKernelConnection, IKernel>();
private kernelsRestarting = new WeakSet<IKernel>();
private kernelReconnectProgress = new WeakSet<IKernel>();
private lastExecutedCellPerKernel = new WeakMap<IKernel, NotebookCell | undefined>();

constructor(
@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry,
@inject(IKernelProvider) private kernelProvider: IKernelProvider
) {}
public activate(): void {
this.kernelProvider.onDidStartKernel(this.onDidStartKernel, this, this.disposableRegistry);
this.disposableRegistry.push(
this.kernelProvider.onDidDisposeKernel((kernel) => {
this.kernelReconnectProgress.delete(kernel);
}, this)
);
this.disposableRegistry.push(
this.kernelProvider.onDidRestartKernel((kernel) => {
this.kernelReconnectProgress.delete(kernel);
}, this)
);
}
private onDidStartKernel(kernel: IKernel) {
if (!this.kernelsStartedSuccessfully.has(kernel)) {
kernel.onPreExecute(
(cell) => this.lastExecutedCellPerKernel.set(kernel, cell),
this,
this.disposableRegistry
);

if (!kernel.session?.kernel) {
return;
}
this.kernelsStartedSuccessfully.add(kernel);
this.kernelConnectionToKernelMapping.set(kernel.session?.kernel, kernel);
kernel.session?.kernel?.connectionStatusChanged.connect(this.onKernelStatusChanged, this);
kernel.addEventHook(async (e) => {
if (e === 'willRestart') {
this.kernelReconnectProgress.delete(kernel);
this.kernelsRestarting.add(kernel);
}
});
kernel.onRestarted(() => this.kernelsRestarting.delete(kernel));
}
}
private onKernelStatusChanged(connection: Kernel.IKernelConnection, connectionStatus: Kernel.ConnectionStatus) {
const kernel = this.kernelConnectionToKernelMapping.get(connection);
if (!kernel) {
return;
}
if (this.kernelsRestarting.has(kernel)) {
return;
}
switch (connectionStatus) {
case 'connected': {
this.kernelReconnectProgress.delete(kernel);
return;
}
case 'disconnected': {
if (this.kernelReconnectProgress.has(kernel)) {
this.kernelReconnectProgress.delete(kernel);
this.onKernelDisconnected(kernel)?.ignoreErrors();
}
return;
}
case 'connecting':
this.kernelReconnectProgress.add(kernel);
return;
default:
return;
}
}
private async onKernelDisconnected(kernel: IKernel) {
const lastExecutedCell = this.lastExecutedCellPerKernel.get(kernel);
sendKernelTelemetryEvent(kernel.resourceUri, Telemetry.KernelCrash);
if (!lastExecutedCell) {
return;
}

const message = isLocalConnection(kernel.kernelConnectionMetadata)
? DataScience.kernelDisconnected()
: DataScience.remoteJupyterConnectionFailedWithServer().format(
getDisplayNameOrNameOfKernelConnection(kernel.kernelConnectionMetadata)
);
await endCellAndDisplayErrorsInCell(lastExecutedCell, kernel.controller, message, false);

// Given the fact that we know the kernel connection is dead, dispose the kernel to clean everything.
await kernel.dispose();
}
}
92 changes: 92 additions & 0 deletions src/kernels/kernelAutoReConnectMonitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import type { Kernel } from '@jupyterlab/services';
import { inject, injectable } from 'inversify';
import { Disposable, ProgressLocation } from 'vscode';
import { IExtensionSyncActivationService } from '../platform/activation/types';
import { IApplicationShell } from '../platform/common/application/types';
import { IDisposable, IDisposableRegistry } from '../platform/common/types';
import { createDeferred } from '../platform/common/utils/async';
import { DataScience } from '../platform/common/utils/localize';
import { noop } from '../platform/common/utils/misc';
import { IStatusProvider } from '../platform/progress/types';
import { getDisplayNameOrNameOfKernelConnection } from './helpers';
import { IKernel, IKernelProvider } from './types';

/**
* In the case of Jupyter kernels, when a kernel dies Jupyter will automatically restart that kernel.
* In such a case we need to display a little progress indicator so user is aware of the fact that the kernel is restarting.
*/
@injectable()
export class KernelAutoReconnectMonitor implements IExtensionSyncActivationService {
private kernelsStartedSuccessfully = new WeakSet<IKernel>();
private kernelConnectionToKernelMapping = new WeakMap<Kernel.IKernelConnection, IKernel>();
private kernelsRestarting = new WeakSet<IKernel>();
private kernelReconnectProgress = new WeakMap<IKernel, IDisposable>();

constructor(
@inject(IStatusProvider) private appShell: IApplicationShell,
@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry,
@inject(IKernelProvider) private kernelProvider: IKernelProvider
) {}
public activate(): void {
this.kernelProvider.onDidStartKernel(this.onDidStartKernel, this, this.disposableRegistry);
this.disposableRegistry.push(
this.kernelProvider.onDidDisposeKernel((kernel) => {
this.kernelReconnectProgress.get(kernel)?.dispose();
this.kernelReconnectProgress.delete(kernel);
}, this)
);
this.disposableRegistry.push(
this.kernelProvider.onDidRestartKernel((kernel) => {
this.kernelReconnectProgress.get(kernel)?.dispose();
this.kernelReconnectProgress.delete(kernel);
}, this)
);
}
private onDidStartKernel(kernel: IKernel) {
if (!this.kernelsStartedSuccessfully.has(kernel)) {
if (!kernel.session?.kernel) {
return;
}
this.kernelsStartedSuccessfully.add(kernel);
this.kernelConnectionToKernelMapping.set(kernel.session?.kernel, kernel);
kernel.session?.kernel?.connectionStatusChanged.connect(this.onKernelStatusChanged, this);
kernel.addEventHook(async (e) => {
if (e === 'willRestart') {
this.kernelReconnectProgress.get(kernel)?.dispose();
this.kernelReconnectProgress.delete(kernel);
this.kernelsRestarting.add(kernel);
}
});
kernel.onRestarted(() => this.kernelsRestarting.delete(kernel));
}
}
private onKernelStatusChanged(connection: Kernel.IKernelConnection, connectionStatus: Kernel.ConnectionStatus) {
const kernel = this.kernelConnectionToKernelMapping.get(connection);
if (!kernel) {
return;
}
if (this.kernelsRestarting.has(kernel)) {
return;
}
if (this.kernelReconnectProgress.has(kernel)) {
if (connectionStatus !== 'connected') {
this.kernelReconnectProgress.get(kernel)?.dispose();
this.kernelReconnectProgress.delete(kernel);
}
return;
}
const deferred = createDeferred<void>();
const message = DataScience.automaticallyReconnectingToAKernelProgressMessage().format(
getDisplayNameOrNameOfKernelConnection(kernel.kernelConnectionMetadata)
);
this.appShell
.withProgress({ location: ProgressLocation.Notification, title: message }, async () => deferred.promise)
.then(noop, noop);

const disposable = new Disposable(() => deferred.resolve());
this.kernelReconnectProgress.set(kernel, disposable);
this.disposableRegistry.push(disposable);
}
}
10 changes: 10 additions & 0 deletions src/kernels/serviceRegistry.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import { Activation } from './activation.node';
import { PortAttributesProviders } from './port/portAttributeProvider.node';
import { ServerPreload } from './jupyter/launcher/serverPreload.node';
import { KernelStartupCodeProvider } from './kernelStartupCodeProvider.node';
import { kernelAutoReConnectFailedMonitor } from './kernelAutoReConnectFailedMonitor';
import { KernelAutoReconnectMonitor } from './kernelAutoReConnectMonitor';

export function registerTypes(serviceManager: IServiceManager, isDevMode: boolean) {
serviceManager.addSingleton<IExtensionSingleActivationService>(IExtensionSingleActivationService, Activation);
Expand Down Expand Up @@ -87,6 +89,14 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea
);
serviceManager.addSingleton<IKernelDependencyService>(IKernelDependencyService, KernelDependencyService);
serviceManager.addSingleton<IExtensionSyncActivationService>(IExtensionSyncActivationService, KernelCrashMonitor);
serviceManager.addSingleton<IExtensionSyncActivationService>(
IExtensionSyncActivationService,
kernelAutoReConnectFailedMonitor
);
serviceManager.addSingleton<IExtensionSyncActivationService>(
IExtensionSyncActivationService,
KernelAutoReconnectMonitor
);
serviceManager.addSingleton<IExtensionSyncActivationService>(
IExtensionSyncActivationService,
KernelAutoRestartMonitor
Expand Down
10 changes: 10 additions & 0 deletions src/kernels/serviceRegistry.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { KernelVariables } from './variables/kernelVariables';
import { JupyterVariables } from './variables/jupyterVariables';
import { PythonVariablesRequester } from './variables/pythonVariableRequester';
import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker';
import { kernelAutoReConnectFailedMonitor } from './kernelAutoReConnectFailedMonitor';
import { KernelAutoReconnectMonitor } from './kernelAutoReConnectMonitor';

@injectable()
class RawNotebookSupportedService implements IRawNotebookSupportedService {
Expand Down Expand Up @@ -59,6 +61,14 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea
serviceManager.addSingleton<IJupyterVariables>(IJupyterVariables, KernelVariables, Identifiers.KERNEL_VARIABLES);

serviceManager.addSingleton<IExtensionSyncActivationService>(IExtensionSyncActivationService, KernelCrashMonitor);
serviceManager.addSingleton<IExtensionSyncActivationService>(
IExtensionSyncActivationService,
kernelAutoReConnectFailedMonitor
);
serviceManager.addSingleton<IExtensionSyncActivationService>(
IExtensionSyncActivationService,
KernelAutoReconnectMonitor
);
serviceManager.addSingleton<IKernelProvider>(IKernelProvider, KernelProvider);
serviceManager.addSingleton<IThirdPartyKernelProvider>(IThirdPartyKernelProvider, ThirdPartyKernelProvider);
serviceManager.addSingleton<PreferredRemoteKernelIdProvider>(
Expand Down
7 changes: 7 additions & 0 deletions src/platform/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,8 @@ export namespace DataScience {
export const restartKernelMessageYes = () => localize('DataScience.restartKernelMessageYes', 'Restart');
export const restartKernelMessageDontAskAgain = () =>
localize('DataScience.restartKernelMessageDontAskAgain', "Don't Ask Again");
export const automaticallyReconnectingToAKernelProgressMessage = () =>
localize('DataScience.automaticallyReconnectingToAKernelProgressMessage', 'Reconnecting to the kernel {0}');
export const restartingKernelStatus = () => localize('DataScience.restartingKernelStatus', 'Restarting Kernel {0}');
export const restartingKernelFailed = () =>
localize(
Expand Down Expand Up @@ -567,6 +569,11 @@ export namespace DataScience {
},
"The Kernel crashed while executing code in the the current cell or a previous cell. Please review the code in the cell(s) to identify a possible cause of the failure. Click <a href='https://aka.ms/vscodeJupyterKernelCrash'>here</a> for more info. View Jupyter [log](command:jupyter.viewOutput) for further details."
);
export const kernelDisconnected = () =>
localize(
'DataScience.kernelDisconnected',
'Unable to connect to the kernel, please verify the Jupyter Server connection. View Jupyter [log](command:jupyter.viewOutput) for further details.'
);
export const cannotRunCellKernelIsDead = () =>
localize(
{ key: 'DataScience.cannotRunCellKernelIsDead', comment: ['{Locked="kernel"}'] },
Expand Down
4 changes: 1 addition & 3 deletions src/platform/errors/errorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,9 +665,7 @@ export function createOutputWithErrorMessageForDisplay(errorMessage: string) {
// Ensure all lines are colored red as errors (except for lines containing hyperlinks).
const stack = errorMessage
.splitLines({ removeEmptyEntries: false, trim: false })
.map((line) => {
return line.includes('<a href') ? line : `\u001b[1;31m${line}`;
})
.map((line) => `\u001b[1;31m${line}`)
.join('\n');
return new NotebookCellOutput([
NotebookCellOutputItem.error({
Expand Down