Skip to content

Commit 97025e2

Browse files
fix: _pending_msgs was negative, iopub.status was called too often
This broke message throttling. Fixes jupyter-widgets#3469
1 parent 32f59ac commit 97025e2

File tree

2 files changed

+93
-14
lines changed

2 files changed

+93
-14
lines changed

packages/base/src/widget.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,12 @@ export class WidgetModel extends Backbone.Model {
333333
if (this.comm !== void 0) {
334334
if (msg.content.execution_state === 'idle') {
335335
this._pending_msgs--;
336+
if (this._pending_msgs < 0) {
337+
console.error(
338+
`Pending messages < 0 (=${this._pending_msgs}), which is unexpected`
339+
);
340+
this._pending_msgs = 0; // do not break message throttling in case of unexpected errors
341+
}
336342
// Send buffer if one is waiting and we are below the throttle.
337343
if (this._msg_buffer !== null && this._pending_msgs < 1) {
338344
const msgId = this.send_sync_message(
@@ -544,11 +550,16 @@ export class WidgetModel extends Backbone.Model {
544550
}
545551
try {
546552
callbacks.iopub = callbacks.iopub || {};
547-
const statuscb = callbacks.iopub.status;
553+
if (callbacks.iopub.previouslyUsedByJupyterWidgets === undefined) {
554+
// Do not break other code that also wants to listen to status updates
555+
callbacks.iopub.statusPrevious = callbacks.iopub.status;
556+
}
557+
// else callbacks.iopub.status is the old handler, that we should not reuse
558+
callbacks.iopub.previouslyUsedByJupyterWidgets = true;
548559
callbacks.iopub.status = (msg: KernelMessage.IStatusMsg): void => {
549560
this._handle_status(msg);
550-
if (statuscb) {
551-
statuscb(msg);
561+
if (callbacks.iopub.statusPrevious) {
562+
callbacks.iopub.statusPrevious(msg);
552563
}
553564
};
554565

packages/base/test/src/widget_test.ts

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -618,31 +618,99 @@ describe('WidgetModel', function () {
618618
});
619619

620620
it('respects the message throttle', function () {
621-
const send = sinon.spy(this.widget, 'send_sync_message');
621+
// reuse the callback, similar to .touch in views, which will
622+
// expose an bug of calling onstatus multiple times
623+
const callbacks = this.widget.callbacks();
622624
this.widget.set('a', 'sync test');
623-
this.widget.save_changes();
625+
expect(this.widget._pending_msgs).to.equal(0);
626+
this.widget.save_changes(callbacks);
627+
expect(this.widget._pending_msgs).to.equal(1);
624628
this.widget.set('a', 'another sync test');
625629
this.widget.set('b', 'change b');
626-
this.widget.save_changes();
630+
this.widget.save_changes(callbacks);
627631
this.widget.set('b', 'change b again');
628-
this.widget.save_changes();
632+
this.widget.save_changes(callbacks);
633+
expect(this.widget._pending_msgs).to.equal(1);
629634

630635
// check that one sync message went through
631-
expect(send).to.be.calledOnce;
632-
expect(send).to.be.calledWith({
633-
a: 'sync test',
636+
expect(this.comm.send).to.be.calledOnce;
637+
expect(this.comm.send).to.be.calledWith({
638+
method: 'update',
639+
state: { a: 'sync test' },
640+
buffer_paths: [],
634641
});
642+
expect(this.widget._msg_buffer).to.deep.equal({
643+
a: 'another sync test',
644+
b: 'change b again',
645+
});
646+
expect(this.widget._msg_buffer_callbacks).to.not.equals(null);
635647
// have the comm send a status idle message
636-
this.widget._handle_status({
648+
callbacks.iopub.status({
637649
content: {
638650
execution_state: 'idle',
639651
},
640652
});
653+
// we directly get a new pending message
654+
expect(this.widget._pending_msgs).to.equal(1);
655+
expect(this.widget._msg_buffer).to.equal(null);
656+
expect(this.widget._msg_buffer_callbacks).to.equals(null);
657+
641658
// check that the other sync message went through with the updated values
642-
expect(send.secondCall).to.be.calledWith({
643-
a: 'another sync test',
644-
b: 'change b again',
659+
expect(this.comm.send.secondCall).to.be.calledWith({
660+
method: 'update',
661+
state: {
662+
a: 'another sync test',
663+
b: 'change b again',
664+
},
665+
buffer_paths: [],
666+
});
667+
callbacks.iopub.status({
668+
content: {
669+
execution_state: 'idle',
670+
},
671+
});
672+
expect(this.widget._pending_msgs).to.equal(0);
673+
674+
// repeat again
675+
this.comm.send.resetHistory();
676+
677+
this.widget.set('a', 'sync test - 2');
678+
this.widget.save_changes(callbacks);
679+
expect(this.widget._pending_msgs).to.equal(1);
680+
this.widget.set('a', 'another sync test - 2');
681+
this.widget.set('b', 'change b - 2');
682+
this.widget.save_changes(callbacks);
683+
this.widget.set('b', 'change b again - 2');
684+
this.widget.save_changes(callbacks);
685+
expect(this.widget._pending_msgs).to.equal(1);
686+
expect(this.comm.send).to.be.calledOnce;
687+
expect(this.comm.send).to.be.calledWith({
688+
method: 'update',
689+
state: { a: 'sync test - 2' },
690+
buffer_paths: [],
691+
});
692+
callbacks.iopub.status({
693+
content: {
694+
execution_state: 'idle',
695+
},
696+
});
697+
// again, directly a new message
698+
expect(this.widget._pending_msgs).to.equal(1);
699+
700+
expect(this.comm.send.secondCall).to.be.calledWith({
701+
method: 'update',
702+
state: {
703+
a: 'another sync test - 2',
704+
b: 'change b again - 2',
705+
},
706+
buffer_paths: [],
707+
});
708+
callbacks.iopub.status({
709+
content: {
710+
execution_state: 'idle',
711+
},
645712
});
713+
expect(this.widget._pending_msgs).to.equal(0);
646714
});
647715

648716
it('Initial values are *not* sent on creation', function () {

0 commit comments

Comments
 (0)