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

Fix race condition in notebook kernel association #13364

Merged
merged 1 commit into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
34 changes: 13 additions & 21 deletions packages/notebook/src/browser/service/notebook-kernel-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,41 +144,42 @@ export class SourceCommand implements Disposable {

const NOTEBOOK_KERNEL_BINDING_STORAGE_KEY = 'notebook.kernel.bindings';
@injectable()
export class NotebookKernelService implements Disposable {
export class NotebookKernelService {

@inject(NotebookService)
protected notebookService: NotebookService;

@inject(StorageService)
protected storageService: StorageService;

private readonly kernels = new Map<string, KernelInfo>();
protected readonly kernels = new Map<string, KernelInfo>();

private notebookBindings: { [key: string]: string } = {};
protected notebookBindings: Record<string, string> = {};

private readonly kernelDetectionTasks = new Map<string, string[]>();
private readonly onDidChangeKernelDetectionTasksEmitter = new Emitter<string>();
protected readonly kernelDetectionTasks = new Map<string, string[]>();
protected readonly onDidChangeKernelDetectionTasksEmitter = new Emitter<string>();
readonly onDidChangeKernelDetectionTasks = this.onDidChangeKernelDetectionTasksEmitter.event;

private readonly onDidChangeSourceActionsEmitter = new Emitter<NotebookSourceActionChangeEvent>();
private readonly kernelSourceActionProviders = new Map<string, KernelSourceActionProvider[]>();
protected readonly onDidChangeSourceActionsEmitter = new Emitter<NotebookSourceActionChangeEvent>();
protected readonly kernelSourceActionProviders = new Map<string, KernelSourceActionProvider[]>();
readonly onDidChangeSourceActions: Event<NotebookSourceActionChangeEvent> = this.onDidChangeSourceActionsEmitter.event;

private readonly onDidAddKernelEmitter = new Emitter<NotebookKernel>();
protected readonly onDidAddKernelEmitter = new Emitter<NotebookKernel>();
readonly onDidAddKernel: Event<NotebookKernel> = this.onDidAddKernelEmitter.event;

private readonly onDidRemoveKernelEmitter = new Emitter<NotebookKernel>();
protected readonly onDidRemoveKernelEmitter = new Emitter<NotebookKernel>();
readonly onDidRemoveKernel: Event<NotebookKernel> = this.onDidRemoveKernelEmitter.event;

private readonly onDidChangeSelectedNotebookKernelBindingEmitter = new Emitter<SelectedNotebookKernelChangeEvent>();
protected readonly onDidChangeSelectedNotebookKernelBindingEmitter = new Emitter<SelectedNotebookKernelChangeEvent>();
readonly onDidChangeSelectedKernel: Event<SelectedNotebookKernelChangeEvent> = this.onDidChangeSelectedNotebookKernelBindingEmitter.event;

private readonly onDidChangeNotebookAffinityEmitter = new Emitter<void>();
protected readonly onDidChangeNotebookAffinityEmitter = new Emitter<void>();
readonly onDidChangeNotebookAffinity: Event<void> = this.onDidChangeNotebookAffinityEmitter.event;

@postConstruct()
init(): void {
this.storageService.getData(NOTEBOOK_KERNEL_BINDING_STORAGE_KEY).then((value: { [key: string]: string } | undefined) => {
this.notebookService.onDidAddNotebookDocument(model => this.tryAutoBindNotebook(model));
this.storageService.getData(NOTEBOOK_KERNEL_BINDING_STORAGE_KEY).then((value: Record<string, string> | undefined) => {
if (value) {
this.notebookBindings = value;
}
Expand Down Expand Up @@ -344,13 +345,4 @@ export class NotebookKernelService implements Disposable {
const allActions = await Promise.all(promises);
return allActions.flat();
}

dispose(): void {
this.onDidChangeKernelDetectionTasksEmitter.dispose();
this.onDidChangeSourceActionsEmitter.dispose();
this.onDidAddKernelEmitter.dispose();
this.onDidRemoveKernelEmitter.dispose();
this.onDidChangeSelectedNotebookKernelBindingEmitter.dispose();
this.onDidChangeNotebookAffinityEmitter.dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { NotebookEditorsMainImpl } from './notebook-editors-main';
import { NotebookDocumentsMainImpl } from './notebook-documents-main';
import { diffMaps, diffSets } from '../../../common/collections';
import { Mutex } from 'async-mutex';
import throttle = require('@theia/core/shared/lodash.throttle');

interface NotebookAndEditorDelta {
removedDocuments: UriComponents[];
Expand Down Expand Up @@ -107,12 +106,12 @@ export class NotebooksAndEditorsMain implements NotebookDocumentsAndEditorsMain
this.notebookEditorService = container.get(NotebookEditorWidgetService);
this.WidgetManager = container.get(WidgetManager);

this.notebookService.onDidAddNotebookDocument(async () => this.throttleStateUpdate(), this, this.disposables);
this.notebookService.onDidRemoveNotebookDocument(async () => this.throttleStateUpdate(), this, this.disposables);
this.notebookService.onDidAddNotebookDocument(async () => this.updateState(), this, this.disposables);
this.notebookService.onDidRemoveNotebookDocument(async () => this.updateState(), this, this.disposables);
// this.WidgetManager.onActiveEditorChanged(() => this.updateState(), this, this.disposables);
this.notebookEditorService.onDidAddNotebookEditor(async editor => this.handleEditorAdd(editor), this, this.disposables);
this.notebookEditorService.onDidRemoveNotebookEditor(async editor => this.handleEditorRemove(editor), this, this.disposables);
this.notebookEditorService.onDidChangeFocusedEditor(async editor => this.throttleStateUpdate(editor), this, this.disposables);
this.notebookEditorService.onDidChangeFocusedEditor(async editor => this.updateState(editor), this, this.disposables);
}

dispose(): void {
Expand All @@ -130,18 +129,16 @@ export class NotebooksAndEditorsMain implements NotebookDocumentsAndEditorsMain
} else {
this.editorListeners.set(editor.id, [disposable]);
}
await this.throttleStateUpdate();
await this.updateState();
}

private handleEditorRemove(editor: NotebookEditorWidget): void {
const listeners = this.editorListeners.get(editor.id);
listeners?.forEach(listener => listener.dispose());
this.editorListeners.delete(editor.id);
this.throttleStateUpdate();
this.updateState();
}

private throttleStateUpdate = throttle((focusedEditor?: NotebookEditorWidget) => this.updateState(focusedEditor), 100);

private async updateState(focusedEditor?: NotebookEditorWidget): Promise<void> {
await this.updateMutex.runExclusive(async () => this.doUpdateState(focusedEditor));
}
Expand Down
8 changes: 4 additions & 4 deletions packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,11 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt {
};
}

$acceptNotebookAssociation(handle: number, uri: UriComponents, value: boolean): void {
async $acceptNotebookAssociation(handle: number, uri: UriComponents, value: boolean): Promise<void> {
const obj = this.kernelData.get(handle);
if (obj) {
// update data structure
const notebook = this.notebooks.getNotebookDocument(URI.from(uri))!;
const notebook = await this.notebooks.waitForNotebookDocument(URI.from(uri));
if (value) {
obj.associatedNotebooks.set(notebook.uri.toString(), true);
} else {
Expand All @@ -320,7 +320,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt {
// extension can dispose kernels in the meantime
return Promise.resolve();
}
const document = this.notebooks.getNotebookDocument(URI.from(uri));
const document = await this.notebooks.waitForNotebookDocument(URI.from(uri));
const cells: theia.NotebookCell[] = [];
for (const cellHandle of handles) {
const cell = document.getCell(cellHandle);
Expand All @@ -347,7 +347,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt {

// cancel or interrupt depends on the controller. When an interrupt handler is used we
// don't trigger the cancelation token of executions.N
const document = this.notebooks.getNotebookDocument(URI.from(uri));
const document = await this.notebooks.waitForNotebookDocument(URI.from(uri));
if (obj.controller.interruptHandler) {
await obj.controller.interruptHandler.call(obj.controller, document.apiNotebook);

Expand Down
20 changes: 20 additions & 0 deletions packages/plugin-ext/src/plugin/notebook/notebooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,26 @@ export class NotebooksExtImpl implements NotebooksExt {
return result;
}

waitForNotebookDocument(uri: TheiaURI, duration = 2000): Promise<NotebookDocument> {
msujew marked this conversation as resolved.
Show resolved Hide resolved
const existing = this.getNotebookDocument(uri, true);
if (existing) {
return Promise.resolve(existing);
}
return new Promise<NotebookDocument>((resolve, reject) => {
const listener = this.onDidOpenNotebookDocument(event => {
if (event.uri.toString() === uri.toString()) {
clearTimeout(timeout);
listener.dispose();
resolve(this.getNotebookDocument(uri));
}
});
const timeout = setTimeout(() => {
listener.dispose();
reject(new Error(`Notebook document did NOT open in ${duration}ms: ${uri}`));
}, duration);
});
}

private createExtHostEditor(document: NotebookDocument, editorId: string, data: NotebookEditorAddData): void {

if (this.editors.has(editorId)) {
Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,8 @@ export function createAPIFactory(
} else {
throw new Error('Invalid arguments');
}
return notebooksExt.getNotebookDocument(uri).apiNotebook;
const result = await notebooksExt.waitForNotebookDocument(uri);
return result.apiNotebook;

},
createFileSystemWatcher: (pattern, ignoreCreate, ignoreChange, ignoreDelete): theia.FileSystemWatcher =>
Expand Down
Loading