Skip to content

Commit c419bfe

Browse files
authored
Merge pull request #3337 from martinRenou/use_control_comm_target_7.x
[Backport 7.x] Use control comm target in LabManager
2 parents 02ae460 + 0296e03 commit c419bfe

File tree

3 files changed

+278
-139
lines changed

3 files changed

+278
-139
lines changed

jupyterlab_widgets/src/manager.ts

+2-82
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,13 @@ import * as Backbone from 'backbone';
66

77
import {
88
ManagerBase, shims, IClassicComm, IWidgetRegistryData, ExportMap,
9-
ExportData, WidgetModel, WidgetView, put_buffers, serialize_state, IStateOptions
9+
ExportData, WidgetModel, WidgetView, serialize_state, IStateOptions
1010
} from '@jupyter-widgets/base';
1111

1212
import {
1313
IDisposable
1414
} from '@lumino/disposable';
1515

16-
import {
17-
PromiseDelegate
18-
} from '@lumino/coreutils';
19-
2016
import {
2117
Widget
2218
} from '@lumino/widgets';
@@ -220,74 +216,11 @@ class WidgetManager extends ManagerBase<Widget> implements IDisposable {
220216
return;
221217
}
222218
await this.context.sessionContext.ready;
223-
// TODO: when we upgrade to @jupyterlab/services 4.1 or later, we can
224-
// remove this 'any' cast.
225219
if (this.context.sessionContext.session?.kernel.handleComms === false) {
226220
return;
227221
}
228-
const comm_ids = await this._get_comm_info();
229-
230-
// For each comm id that we do not know about, create the comm, and request the state.
231-
const widgets_info = await Promise.all(Object.keys(comm_ids).map(async (comm_id) => {
232-
try {
233-
await this.get_model(comm_id);
234-
// If we successfully get the model, do no more.
235-
return;
236-
} catch (e) {
237-
// If we have the widget model not found error, then we can create the
238-
// widget. Otherwise, rethrow the error. We have to check the error
239-
// message text explicitly because the get_model function in this
240-
// class throws a generic error with this specific text.
241-
if (e.message !== 'widget model not found') {
242-
throw e;
243-
}
244-
const comm = await this._create_comm(this.comm_target_name, comm_id);
245-
246-
let msg_id: string;
247-
const info = new PromiseDelegate<Private.ICommUpdateData>();
248-
comm.on_msg((msg: KernelMessage.ICommMsgMsg) => {
249-
if ((msg.parent_header as any).msg_id === msg_id
250-
&& msg.header.msg_type === 'comm_msg'
251-
&& msg.content.data.method === 'update') {
252-
let data = (msg.content.data as any);
253-
let buffer_paths = data.buffer_paths || [];
254-
// Make sure the buffers are DataViews
255-
let buffers = (msg.buffers || []).map(b => {
256-
if (b instanceof DataView) {
257-
return b;
258-
} else {
259-
return new DataView(b instanceof ArrayBuffer ? b : b.buffer);
260-
}
261-
});
262-
put_buffers(data.state, buffer_paths, buffers);
263-
info.resolve({comm, msg});
264-
}
265-
});
266-
msg_id = comm.send({
267-
method: 'request_state'
268-
}, this.callbacks(undefined));
269222

270-
return info.promise;
271-
}
272-
}));
273-
274-
// We put in a synchronization barrier here so that we don't have to
275-
// topologically sort the restored widgets. `new_model` synchronously
276-
// registers the widget ids before reconstructing their state
277-
// asynchronously, so promises to every widget reference should be available
278-
// by the time they are used.
279-
await Promise.all(widgets_info.map(async widget_info => {
280-
if (!widget_info) {
281-
return;
282-
}
283-
const content = widget_info.msg.content as any;
284-
await this.new_model({
285-
model_name: content.data.state._model_name,
286-
model_module: content.data.state._model_module,
287-
model_module_version: content.data.state._model_module_version,
288-
comm: widget_info.comm,
289-
}, content.data.state);
290-
}));
223+
return super._loadFromKernel();
291224
}
292225

293226

@@ -538,16 +471,3 @@ namespace WidgetManager {
538471
saveState: boolean
539472
};
540473
}
541-
542-
543-
namespace Private {
544-
545-
/**
546-
* Data promised when a comm info request resolves.
547-
*/
548-
export
549-
interface ICommUpdateData {
550-
comm: IClassicComm;
551-
msg: KernelMessage.ICommMsgMsg;
552-
}
553-
}

packages/base/src/manager-base.ts

+258
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
import * as utils from './utils';
55
import * as services from '@jupyterlab/services';
66

7+
import {
8+
PromiseDelegate,
9+
} from '@lumino/coreutils';
10+
711
import {
812
DOMWidgetView, WidgetModel, WidgetView, DOMWidgetModel
913
} from './widget';
@@ -18,6 +22,21 @@ import {
1822

1923
const PROTOCOL_MAJOR_VERSION = PROTOCOL_VERSION.split('.', 1)[0];
2024

25+
/**
26+
* The control comm target name.
27+
*/
28+
export const CONTROL_COMM_TARGET = 'jupyter.widget.control';
29+
30+
/**
31+
* The supported version for the control comm channel.
32+
*/
33+
export const CONTROL_COMM_PROTOCOL_VERSION = '1.0.0';
34+
35+
/**
36+
* Time (in ms) after which we consider the control comm target not responding.
37+
*/
38+
export const CONTROL_COMM_TIMEOUT = 4000;
39+
2140
/**
2241
* The options for a model.
2342
*
@@ -361,7 +380,236 @@ abstract class ManagerBase<T> {
361380
widget_model.name = options.model_name;
362381
widget_model.module = options.model_module;
363382
return widget_model;
383+
}
384+
385+
/**
386+
* Fetch all widgets states from the kernel using the control comm channel
387+
* If this fails (control comm handler not implemented kernel side),
388+
* it will fall back to `_loadFromKernelModels`.
389+
*
390+
* This is a utility function that can be used in subclasses.
391+
*/
392+
protected async _loadFromKernel(): Promise<void> {
393+
// Try fetching all widget states through the control comm
394+
let data: any;
395+
let buffers: any;
396+
try {
397+
const initComm = await this._create_comm(
398+
CONTROL_COMM_TARGET,
399+
utils.uuid(),
400+
{},
401+
{ version: CONTROL_COMM_PROTOCOL_VERSION }
402+
);
403+
404+
await new Promise((resolve, reject) => {
405+
initComm.on_msg((msg: any) => {
406+
data = msg['content']['data'];
407+
408+
if (data.method !== 'update_states') {
409+
console.warn(`
410+
Unknown ${data.method} message on the Control channel
411+
`);
412+
return;
413+
}
414+
415+
buffers = (msg.buffers || []).map((b: any) => {
416+
if (b instanceof DataView) {
417+
return b;
418+
} else {
419+
return new DataView(b instanceof ArrayBuffer ? b : b.buffer);
420+
}
421+
});
422+
423+
resolve(null);
424+
});
425+
426+
initComm.on_close(() => reject('Control comm was closed too early'));
427+
428+
// Send a states request msg
429+
initComm.send({ method: 'request_states' }, {});
430+
431+
// Reject if we didn't get a response in time
432+
setTimeout(
433+
() => reject('Control comm did not respond in time'),
434+
CONTROL_COMM_TIMEOUT
435+
);
436+
});
437+
438+
initComm.close();
439+
} catch (error) {
440+
console.warn(
441+
'Failed to fetch ipywidgets through the "jupyter.widget.control" comm channel, fallback to fetching individual model state. Reason:',
442+
error
443+
);
444+
// Fall back to the old implementation for old ipywidgets backend versions (ipywidgets<=7.6)
445+
return this._loadFromKernelModels();
446+
}
447+
448+
const states: any = data.states;
449+
const bufferPaths: any = {};
450+
const bufferGroups: any = {};
451+
452+
// Group buffers and buffer paths by widget id
453+
for (let i = 0; i < data.buffer_paths.length; i++) {
454+
const [widget_id, ...path] = data.buffer_paths[i];
455+
const b = buffers[i];
456+
if (!bufferPaths[widget_id]) {
457+
bufferPaths[widget_id] = [];
458+
bufferGroups[widget_id] = [];
459+
}
460+
bufferPaths[widget_id].push(path);
461+
bufferGroups[widget_id].push(b);
462+
}
463+
464+
// Create comms for all new widgets.
465+
let widget_comms = await Promise.all(
466+
Object.keys(states).map(async (widget_id) => {
467+
let comm = undefined;
468+
let modelPromise = undefined;
469+
try {
470+
modelPromise = this.get_model(widget_id);
471+
if (modelPromise === undefined) {
472+
comm = await this._create_comm('jupyter.widget', widget_id);
473+
} else {
474+
// For JLab, the promise is rejected, so we have to await to
475+
// find out if it is actually a model.
476+
await modelPromise;
477+
}
478+
} catch (e) {
479+
// The JLab widget manager will throw an error with this specific error message.
480+
if (e.message !== 'widget model not found') {
481+
throw e;
482+
}
483+
comm = await this._create_comm('jupyter.widget', widget_id);
484+
}
485+
return {widget_id, comm}
486+
})
487+
)
488+
489+
await Promise.all(widget_comms.map(async ({widget_id, comm}) => {
490+
const state = states[widget_id];
491+
// Put binary buffers
492+
if (widget_id in bufferPaths) {
493+
utils.put_buffers(
494+
state,
495+
bufferPaths[widget_id],
496+
bufferGroups[widget_id]
497+
);
498+
}
499+
try {
364500

501+
if (comm === undefined) {
502+
// model already exists here
503+
const model = await this.get_model(widget_id);
504+
model!.set_state(state.state);
505+
} else {
506+
// This must be the first await in the code path that
507+
// reaches here so that registering the model promise in
508+
// new_model can register the widget promise before it may
509+
// be required by other widgets.
510+
await this.new_model(
511+
{
512+
model_name: state.model_name,
513+
model_module: state.model_module,
514+
model_module_version: state.model_module_version,
515+
model_id: widget_id,
516+
comm: comm,
517+
},
518+
state.state
519+
);
520+
}
521+
522+
} catch (error) {
523+
// Failed to create a widget model, we continue creating other models so that
524+
// other widgets can render
525+
console.error(error);
526+
}
527+
}));
528+
}
529+
530+
/**
531+
* Old implementation of fetching widget models one by one using
532+
* the request_state message on each comm.
533+
*
534+
* This is a utility function that can be used in subclasses.
535+
*/
536+
protected async _loadFromKernelModels(): Promise<void> {
537+
const comm_ids = await this._get_comm_info();
538+
539+
// For each comm id that we do not know about, create the comm, and request the state.
540+
const widgets_info = await Promise.all(
541+
Object.keys(comm_ids).map(async (comm_id) => {
542+
try {
543+
const model = this.get_model(comm_id);
544+
// TODO Have the same this.get_model implementation for
545+
// the widgetsnbextension and labextension, the one that
546+
// throws an error if the model is not found instead of
547+
// returning undefined
548+
if (model === undefined) {
549+
throw new Error('widget model not found');
550+
}
551+
await model;
552+
// If we successfully get the model, do no more.
553+
return;
554+
} catch (e) {
555+
// If we have the widget model not found error, then we can create the
556+
// widget. Otherwise, rethrow the error. We have to check the error
557+
// message text explicitly because the get_model function in this
558+
// class throws a generic error with this specific text.
559+
if (e.message !== 'widget model not found') {
560+
throw e;
561+
}
562+
const comm = await this._create_comm(this.comm_target_name, comm_id);
563+
564+
let msg_id = '';
565+
const info = new PromiseDelegate<Private.ICommUpdateData>();
566+
comm.on_msg((msg) => {
567+
if (
568+
(msg.parent_header as any).msg_id === msg_id &&
569+
msg.header.msg_type === 'comm_msg' &&
570+
msg.content.data.method === 'update'
571+
) {
572+
const data = msg.content.data as any;
573+
const buffer_paths = data.buffer_paths || [];
574+
const buffers = msg.buffers || [];
575+
utils.put_buffers(data.state, buffer_paths, buffers);
576+
info.resolve({ comm, msg });
577+
}
578+
});
579+
msg_id = comm.send(
580+
{
581+
method: 'request_state',
582+
},
583+
this.callbacks(undefined)
584+
);
585+
586+
return info.promise;
587+
}
588+
})
589+
);
590+
591+
// We put in a synchronization barrier here so that we don't have to
592+
// topologically sort the restored widgets. `new_model` synchronously
593+
// registers the widget ids before reconstructing their state
594+
// asynchronously, so promises to every widget reference should be available
595+
// by the time they are used.
596+
await Promise.all(
597+
widgets_info.map(async (widget_info) => {
598+
if (!widget_info) {
599+
return;
600+
}
601+
const content = widget_info.msg.content as any;
602+
await this.new_model(
603+
{
604+
model_name: content.data.state._model_name,
605+
model_module: content.data.state._model_module,
606+
model_module_version: content.data.state._model_module_version,
607+
comm: widget_info.comm,
608+
},
609+
content.data.state
610+
);
611+
})
612+
);
365613
}
366614

367615
/**
@@ -586,3 +834,13 @@ function serialize_state(models: WidgetModel[], options: IStateOptions = {}) {
586834
});
587835
return {version_major: 2, version_minor: 0, state: state};
588836
}
837+
838+
namespace Private {
839+
/**
840+
* Data promised when a comm info request resolves.
841+
*/
842+
export interface ICommUpdateData {
843+
comm: IClassicComm;
844+
msg: services.KernelMessage.ICommMsgMsg;
845+
}
846+
}

0 commit comments

Comments
 (0)