[FIX] bus: elect master tab when master tab is unloaded

Before this commit, chat windows could not be sync'ed
between browser tabs for about 10 to 20 seconds.

This issue is caused by the implementation of the
crosstab longpolling, which uses a master-slave
architecture with a single tab being the master tab.
The master tab is responsible for performing the
longpolling RPC, so that it keeps a single HTTP request
for all Odoo tabs. Other tabs are synchronized through
the local storage.

When the master tab was closed, a new master tab election
must occur. However, this process was taking up to 20 seconds,
due to the tab waiting for their next tick on the local
storage to initiate a new election. The tick for a slave
tab is 10 seconds, and it may jumps a tick in case the master
tab is closed exactly before the tick, hence extending the
duration to 20 seconds.

This commit fixes the problem by ensuring that the tab
election for the longpolling is done right after the master
tab is closed. As a result, a new master tab should follow
right away.

Steps to reproduce:

1. open two tabs.
2. open a chat window in one tab: it opens in both tabs.
3. fold/unfold chat window in one tab: both tab should sync.
4. refresh both tabs.
5. fold/unfold chat window in one tab: both tab should not sync.
6. after 10 or 20 seconds, both tabs should again sync.

Task-ID 1911437

closes odoo/odoo#28925
This commit is contained in:
Alexandre Kühn
2018-11-21 16:43:27 +00:00
parent dbd5cb48e2
commit b4f132ca21
2 changed files with 106 additions and 3 deletions
+4 -3
View File
@@ -105,7 +105,7 @@ var CrossTabBus = Longpolling.extend({
$(window).on('unload.' + this._id, this._onUnload.bind(this));
if (!this._callLocalStorage('getItem', 'peers')) {
if (!this._callLocalStorage('getItem', 'master')) {
this._startElection();
}
@@ -252,6 +252,7 @@ var CrossTabBus = Longpolling.extend({
//we're next in queue. Electing as master
this.lastHeartbeat = now;
this._callLocalStorage('setItem', 'heartbeat', this.lastHeartbeat);
this._callLocalStorage('setItem', 'master', true);
this._isMasterTab = true;
this.startPolling();
this.trigger('become_master');
@@ -297,7 +298,7 @@ var CrossTabBus = Longpolling.extend({
var value = JSON.parse(e.newValue);
var key = e.key;
if (this._isRegistered && key === this._generateKey('peers') && !value) {
if (this._isRegistered && key === this._generateKey('master') && !value) {
//master was unloaded
this._startElection();
}
@@ -341,7 +342,7 @@ var CrossTabBus = Longpolling.extend({
// unload master
if (this._isMasterTab) {
this._callLocalStorage('removeItem', 'peers');
this._callLocalStorage('removeItem', 'master');
}
},
});
+102
View File
@@ -214,4 +214,106 @@ QUnit.module('Bus', {
}, 3);
});
QUnit.test('cross tab bus elect new master on master unload', function (assert) {
var done = assert.async();
assert.expect(8);
// master
var pollDeferredMaster = $.Deferred();
var parentMaster = new Widget();
testUtils.addMockEnvironment(parentMaster, {
data: {},
services: {
bus_service: BusService,
local_storage: LocalStorageServiceMock,
},
mockRPC: function (route, args) {
if (route === '/longpolling/poll') {
assert.step(['master', route, args.channels.join(',')]);
pollDeferredMaster = $.Deferred();
pollDeferredMaster.abort = (function () {
this.reject({message: "XmlHttpRequestError abort"}, $.Event());
}).bind(pollDeferredMaster);
return pollDeferredMaster;
}
return this._super.apply(this, arguments);
}
});
var master = new Widget(parentMaster);
master.appendTo($('#qunit-fixture'));
master.call('bus_service', 'onNotification', master, function (notifications) {
assert.step(['master', 'notification', notifications]);
});
master.call('bus_service', 'addChannel', 'lambda');
// slave
setTimeout(function () {
var parentSlave = new Widget();
var pollDeferredSlave = $.Deferred();
testUtils.addMockEnvironment(parentSlave, {
data: {},
services: {
bus_service: BusService,
local_storage: LocalStorageServiceMock,
},
mockRPC: function (route, args) {
if (route === '/longpolling/poll') {
assert.step(['slave', route, args.channels.join(',')]);
pollDeferredSlave = $.Deferred();
pollDeferredSlave.abort = (function () {
this.reject({message: "XmlHttpRequestError abort"}, $.Event());
}).bind(pollDeferredSlave);
return pollDeferredSlave;
}
return this._super.apply(this, arguments);
}
});
var slave = new Widget(parentSlave);
slave.appendTo($('#qunit-fixture'));
slave.call('bus_service', 'onNotification', slave, function (notifications) {
assert.step(['slave', 'notification', notifications]);
});
slave.call('bus_service', 'addChannel', 'lambda');
pollDeferredMaster.resolve([{
id: 1,
channel: 'lambda',
message: 'beta',
}]);
// simulate unloading master
master.call('bus_service', '_onUnload');
pollDeferredSlave.resolve([{
id: 2,
channel: 'lambda',
message: 'gamma',
}]);
assert.verifySteps([
["master", "/longpolling/poll", "lambda"],
["master", "notification", [["lambda", "beta"]]],
["slave", "notification", [["lambda", "beta"]]],
["master", "/longpolling/poll", "lambda"],
["slave", "/longpolling/poll", "lambda"],
["slave", "notification", [["lambda", "gamma"]]],
["slave", "/longpolling/poll", "lambda"],
]);
parentMaster.destroy();
parentSlave.destroy();
done();
}, 3);
});
});});