Skip to content

Commit fbdc7f8

Browse files
authored
Merge pull request #3470 from maartenbreddels/fix_message_throttle_negative
fix: _pending_msgs was negative, iopub.status was called too often
2 parents 7c1ae90 + 97025e2 commit fbdc7f8

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
@@ -334,6 +334,12 @@ export class WidgetModel extends Backbone.Model {
334334
if (this.comm !== void 0) {
335335
if (msg.content.execution_state === 'idle') {
336336
this._pending_msgs--;
337+
if (this._pending_msgs < 0) {
338+
console.error(
339+
`Pending messages < 0 (=${this._pending_msgs}), which is unexpected`
340+
);
341+
this._pending_msgs = 0; // do not break message throttling in case of unexpected errors
342+
}
337343
// Send buffer if one is waiting and we are below the throttle.
338344
if (this._msg_buffer !== null && this._pending_msgs < 1) {
339345
const msgId = this.send_sync_message(
@@ -548,11 +554,16 @@ export class WidgetModel extends Backbone.Model {
548554
}
549555
try {
550556
callbacks.iopub = callbacks.iopub || {};
551-
const statuscb = callbacks.iopub.status;
557+
if (callbacks.iopub.previouslyUsedByJupyterWidgets === undefined) {
558+
// Do not break other code that also wants to listen to status updates
559+
callbacks.iopub.statusPrevious = callbacks.iopub.status;
560+
}
561+
// else callbacks.iopub.status is the old handler, that we should not reuse
562+
callbacks.iopub.previouslyUsedByJupyterWidgets = true;
552563
callbacks.iopub.status = (msg: KernelMessage.IStatusMsg): void => {
553564
this._handle_status(msg);
554-
if (statuscb) {
555-
statuscb(msg);
565+
if (callbacks.iopub.statusPrevious) {
566+
callbacks.iopub.statusPrevious(msg);
556567
}
557568
};
558569

packages/base/test/src/widget_test.ts

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

618618
it('respects the message throttle', function () {
619-
const send = sinon.spy(this.widget, 'send_sync_message');
619+
// reuse the callback, similar to .touch in views, which will
620+
// expose an bug of calling onstatus multiple times
621+
const callbacks = this.widget.callbacks();
620622
this.widget.set('a', 'sync test');
621-
this.widget.save_changes();
623+
expect(this.widget._pending_msgs).to.equal(0);
624+
this.widget.save_changes(callbacks);
625+
expect(this.widget._pending_msgs).to.equal(1);
622626
this.widget.set('a', 'another sync test');
623627
this.widget.set('b', 'change b');
624-
this.widget.save_changes();
628+
this.widget.save_changes(callbacks);
625629
this.widget.set('b', 'change b again');
626-
this.widget.save_changes();
630+
this.widget.save_changes(callbacks);
631+
expect(this.widget._pending_msgs).to.equal(1);
627632

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

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

0 commit comments

Comments
 (0)