1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-12-20 22:21:59 +03:00

Avoid a packetstorm of device queries on startup

Two main changes here:
 * when we get an m.new_device event for a device we know about, ignore it
 * Batch up the m.new_device events received during initialsync and spam out
   all the queries at once.
This commit is contained in:
Richard van der Hoff
2016-11-17 15:31:23 +00:00
parent 749f53a22b
commit 036d1da013
4 changed files with 180 additions and 63 deletions

View File

@@ -88,15 +88,6 @@ EncryptionAlgorithm.prototype.onRoomMembership = function(
event, member, oldMembership event, member, oldMembership
) {}; ) {};
/**
* Called when a new device announces itself in the room
*
* @param {string} userId owner of the device
* @param {string} deviceId deviceId of the device
*/
EncryptionAlgorithm.prototype.onNewDevice = function(userId, deviceId) {};
/** /**
* base type for decryption implementations * base type for decryption implementations
* *

View File

@@ -54,6 +54,10 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
this._userId = userId; this._userId = userId;
this._deviceId = deviceId; this._deviceId = deviceId;
this._initialSyncCompleted = false;
// userId -> deviceId -> true
this._pendingNewDevices = {};
this._olmDevice = new OlmDevice(sessionStore); this._olmDevice = new OlmDevice(sessionStore);
// EncryptionAlgorithm instance for each room // EncryptionAlgorithm instance for each room
@@ -272,24 +276,25 @@ Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
// map from userid -> deviceid -> DeviceInfo // map from userid -> deviceid -> DeviceInfo
var stored = {}; var stored = {};
function storeDev(userId, dev) {
stored[userId][dev.deviceId] = dev;
}
// list of userids we need to download keys for // list of userids we need to download keys for
var downloadUsers = []; var downloadUsers = [];
for (var i = 0; i < userIds.length; ++i) { if (forceDownload) {
var userId = userIds[i]; downloadUsers = userIds;
stored[userId] = {}; } else {
for (var i = 0; i < userIds.length; ++i) {
var userId = userIds[i];
var devices = this.getStoredDevicesForUser(userId);
var devices = this.getStoredDevicesForUser(userId); if (!devices) {
downloadUsers.push(userId);
if (!devices || forceDownload) { } else {
downloadUsers.push(userId); stored[userId] = {};
} devices.map(storeDev.bind(null, userId));
if (devices) {
for (var j = 0; j < devices.length; ++j) {
var dev = devices[j];
stored[userId][dev.deviceId] = dev;
} }
} }
} }
@@ -298,30 +303,79 @@ Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
return q(stored); return q(stored);
} }
return this._baseApis.downloadKeysForUsers( var r = this._doKeyDownloadForUsers(downloadUsers);
var promises = [];
downloadUsers.map(function(u) {
promises.push(r[u].catch(function(e) {
console.warn('Error downloading keys for user ' + u + ':', e);
}).then(function() {
stored[u] = {};
var devices = self.getStoredDevicesForUser(u) || [];
devices.map(storeDev.bind(null, u));
}));
});
return q.all(promises).then(function() {
return stored;
});
};
/**
* @param {string[]} downloadUsers list of userIds
*
* @return {Object a map from userId to a promise for a result for that user
*/
Crypto.prototype._doKeyDownloadForUsers = function(downloadUsers) {
var self = this;
console.log('Starting key download for ' + downloadUsers);
var deferMap = {};
var promiseMap = {};
downloadUsers.map(function(u) {
deferMap[u] = q.defer();
promiseMap[u] = deferMap[u].promise;
});
this._baseApis.downloadKeysForUsers(
downloadUsers downloadUsers
).then(function(res) { ).done(function(res) {
var dk = res.device_keys || {}; var dk = res.device_keys || {};
for (var i = 0; i < downloadUsers.length; ++i) { for (var i = 0; i < downloadUsers.length; ++i) {
var userId = downloadUsers[i]; var userId = downloadUsers[i];
// console.log('keys for ' + userId + ':', dk[userId]); var deviceId;
console.log('got keys for ' + userId + ':', dk[userId]);
if (!dk[userId]) { if (!dk[userId]) {
// no result for this user // no result for this user
// TODO: do something with failures var err = 'Unknown';
// TODO: do something with res.failures
deferMap[userId].reject(err);
continue; continue;
} }
// map from deviceid -> deviceinfo for this user // map from deviceid -> deviceinfo for this user
var userStore = stored[userId]; var userStore = {};
var devs = self._sessionStore.getEndToEndDevicesForUser(userId);
if (devs) {
for (deviceId in devs) {
if (devs.hasOwnProperty(deviceId)) {
var d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
userStore[deviceId] = d;
}
}
}
_updateStoredDeviceKeysForUser( _updateStoredDeviceKeysForUser(
self._olmDevice, userId, userStore, dk[userId] self._olmDevice, userId, userStore, dk[userId]
); );
// update the session store // update the session store
var storage = {}; var storage = {};
for (var deviceId in userStore) { for (deviceId in userStore) {
if (!userStore.hasOwnProperty(deviceId)) { if (!userStore.hasOwnProperty(deviceId)) {
continue; continue;
} }
@@ -331,9 +385,16 @@ Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
self._sessionStore.storeEndToEndDevicesForUser( self._sessionStore.storeEndToEndDevicesForUser(
userId, storage userId, storage
); );
deferMap[userId].resolve();
} }
return stored; }, function(err) {
downloadUsers.map(function(u) {
deferMap[u].reject(err);
});
}); });
return promiseMap;
}; };
function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore, function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
@@ -462,6 +523,22 @@ Crypto.prototype.getStoredDevicesForUser = function(userId) {
return res; return res;
}; };
/**
* Get the stored keys for a single device
*
* @param {string} userId
* @param {string} deviceId
*
* @return {module:crypto/deviceinfo?} list of devices, or undefined
* if we don't know about this device
*/
Crypto.prototype.getStoredDevice = function(userId, deviceId) {
var devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devs || !devs[deviceId]) {
return undefined;
}
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
};
/** /**
* List the stored device keys for a user id * List the stored device keys for a user id
@@ -998,6 +1075,11 @@ Crypto.prototype._onCryptoEvent = function(event) {
* @param {module:models/room[]} rooms list of rooms the client knows about * @param {module:models/room[]} rooms list of rooms the client knows about
*/ */
Crypto.prototype._onInitialSyncCompleted = function(rooms) { Crypto.prototype._onInitialSyncCompleted = function(rooms) {
this._initialSyncCompleted = true;
// catch up on any m.new_device events which arrived during the initial sync.
this._flushNewDeviceRequests();
if (this._sessionStore.getDeviceAnnounced()) { if (this._sessionStore.getDeviceAnnounced()) {
return; return;
} }
@@ -1124,26 +1206,58 @@ Crypto.prototype._onNewDeviceEvent = function(event) {
console.log("m.new_device event from " + userId + ":" + deviceId + console.log("m.new_device event from " + userId + ":" + deviceId +
" for rooms " + rooms); " for rooms " + rooms);
if (this.getStoredDevice(userId, deviceId)) {
console.log("Known device; ignoring");
return;
}
this._pendingNewDevices[userId] = this._pendingNewDevices[userId] || {};
this._pendingNewDevices[userId][deviceId] = true;
// we delay handling these until the intialsync has completed, so that we
// can do all of them together.
if (this._initialSyncCompleted) {
this._flushNewDeviceRequests();
}
};
/**
* Start device queries for any users who sent us an m.new_device recently
*/
Crypto.prototype._flushNewDeviceRequests = function() {
var self = this; var self = this;
this.downloadKeys(
[userId], true var pending = this._pendingNewDevices;
).then(function() { var users = utils.keys(pending).filter(function(u) {
for (var i = 0; i < rooms.length; i++) { return utils.keys(pending[u]).length > 0;
var roomId = rooms[i]; });
var alg = self._roomEncryptors[roomId];
if (!alg) { if (users.length === 0) {
// not encrypting in this room return;
continue; }
}
alg.onNewDevice(userId, deviceId); var r = this._doKeyDownloadForUsers(users);
}
}).catch(function(e) { // we've kicked off requests to these users: remove their
console.error( // pending flag for now.
"Error updating device keys for new device " + userId + ":" + this._pendingNewDevices = {};
deviceId,
e users.map(function(u) {
); r[u] = r[u].catch(function(e) {
}).done(); console.error(
'Error updating device keys for user ' + u + ':', e
);
// reinstate the pending flags on any users which failed; this will
// mean that we will do another download in the future, but won't
// tight-loop.
//
self._pendingNewDevices[u] = self._pendingNewDevices[u] || {};
utils.update(self._pendingNewDevices[u], pending[u]);
});
});
q.all(utils.values(r)).done();
}; };
/** /**

View File

@@ -739,4 +739,31 @@ describe("MatrixClient crypto", function() {
}).then(aliRecvMessage) }).then(aliRecvMessage)
.catch(test_utils.failTest).done(done); .catch(test_utils.failTest).done(done);
}); });
it("Ali does a key query when she gets a new_device event", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(function() {
var syncData = {
next_batch: '2',
to_device: {
events: [
test_utils.mkEvent({
content: {
device_id: 'TEST_DEVICE',
rooms: [],
},
sender: bobUserId,
type: 'm.new_device',
}),
],
},
};
aliHttpBackend.when('GET', '/sync').respond(200, syncData);
return aliHttpBackend.flush('/sync', 1);
}).then(expectAliQueryKeys)
.nodeify(done);
});
}); });

View File

@@ -371,21 +371,6 @@ describe("MatrixClient", function() {
httpBackend.flush(); httpBackend.flush();
}); });
it("should return a rejected promise if the request fails", function(done) {
httpBackend.when("POST", "/keys/query").respond(400);
var exceptionThrown;
client.downloadKeys(["bottom"]).then(function() {
fail("download didn't fail");
}, function(err) {
exceptionThrown = err;
}).then(function() {
expect(exceptionThrown).toBeTruthy();
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
}); });
describe("deleteDevice", function() { describe("deleteDevice", function() {