Skip to content

Commit d3fe0a4

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

File tree

2 files changed

+94
-14
lines changed

2 files changed

+94
-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: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -618,31 +618,100 @@ 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+
675+
// repeat again
676+
this.comm.send.resetHistory();
677+
678+
this.widget.set('a', 'sync test - 2');
679+
this.widget.save_changes(callbacks);
680+
expect(this.widget._pending_msgs).to.equal(1);
681+
this.widget.set('a', 'another sync test - 2');
682+
this.widget.set('b', 'change b - 2');
683+
this.widget.save_changes(callbacks);
684+
this.widget.set('b', 'change b again - 2');
685+
this.widget.save_changes(callbacks);
686+
expect(this.widget._pending_msgs).to.equal(1);
687+
expect(this.comm.send).to.be.calledOnce;
688+
expect(this.comm.send).to.be.calledWith({
689+
method: 'update',
690+
state: { a: 'sync test - 2' },
691+
buffer_paths: [],
692+
});
693+
callbacks.iopub.status({
694+
content: {
695+
execution_state: 'idle',
696+
},
697+
});
698+
// again, directly a new message
699+
expect(this.widget._pending_msgs).to.equal(1);
700+
701+
expect(this.comm.send.secondCall).to.be.calledWith({
702+
method: 'update',
703+
state: {
704+
a: 'another sync test - 2',
705+
b: 'change b again - 2',
706+
},
707+
buffer_paths: [],
708+
});
709+
callbacks.iopub.status({
710+
content: {
711+
execution_state: 'idle',
712+
},
645713
});
714+
expect(this.widget._pending_msgs).to.equal(0);
646715
});
647716

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

0 commit comments

Comments
 (0)