Skip to content

Commit

Permalink
Merge pull request #884 from jryans/degraded-storage
Browse files Browse the repository at this point in the history
Degrade `IndexedDBStore` back to memory only on failure
  • Loading branch information
jryans authored Apr 5, 2019
2 parents 941d93c + 389fcfa commit b1b4941
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 26 deletions.
3 changes: 3 additions & 0 deletions spec/unit/room.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,9 @@ describe("Room", function() {
// events should already be MatrixEvents
return function(event) {return event;};
},
isCryptoEnabled() {
return true;
},
isRoomEncrypted: function() {
return false;
},
Expand Down
2 changes: 1 addition & 1 deletion src/models/room.js
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ Room.prototype.loadMembersIfNeeded = function() {
const inMemoryUpdate = this._loadMembers().then((result) => {
this.currentState.setOutOfBandMembers(result.memberEvents);
// now the members are loaded, start to track the e2e devices if needed
if (this._client.isRoomEncrypted(this.roomId)) {
if (this._client.isCryptoEnabled() && this._client.isRoomEncrypted(this.roomId)) {
this._client._crypto.trackRoomDevices(this.roomId);
}
return result.fromServer;
Expand Down
103 changes: 78 additions & 25 deletions src/store/indexeddb.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

/* eslint-disable no-invalid-this */

import Promise from 'bluebird';
import {MemoryStore} from "./memory";
import utils from "../utils";
import {EventEmitter} from 'events';
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
import RemoteIndexedDBStoreBackend from "./indexeddb-remote-backend.js";
import User from "../models/user";
Expand Down Expand Up @@ -110,6 +113,7 @@ const IndexedDBStore = function IndexedDBStore(opts) {
};
};
utils.inherits(IndexedDBStore, MemoryStore);
utils.extend(IndexedDBStore.prototype, EventEmitter.prototype);

IndexedDBStore.exists = function(indexedDB, dbName) {
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
Expand Down Expand Up @@ -146,36 +150,36 @@ IndexedDBStore.prototype.startup = function() {
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
IndexedDBStore.prototype.getSavedSync = function() {
IndexedDBStore.prototype.getSavedSync = degradable(function() {
return this.backend.getSavedSync();
};
}, "getSavedSync");

/** @return {Promise<bool>} whether or not the database was newly created in this session. */
IndexedDBStore.prototype.isNewlyCreated = function() {
IndexedDBStore.prototype.isNewlyCreated = degradable(function() {
return this.backend.isNewlyCreated();
};
}, "isNewlyCreated");

/**
* @return {Promise} If there is a saved sync, the nextBatch token
* for this sync, otherwise null.
*/
IndexedDBStore.prototype.getSavedSyncToken = function() {
IndexedDBStore.prototype.getSavedSyncToken = degradable(function() {
return this.backend.getNextBatchToken();
},
}, "getSavedSyncToken"),

/**
* Delete all data from this store.
* @return {Promise} Resolves if the data was deleted from the database.
*/
IndexedDBStore.prototype.deleteAllData = function() {
IndexedDBStore.prototype.deleteAllData = degradable(function() {
MemoryStore.prototype.deleteAllData.call(this);
return this.backend.clearDatabase().then(() => {
console.log("Deleted indexeddb data.");
}, (err) => {
console.error(`Failed to delete indexeddb data: ${err}`);
throw err;
});
};
});

/**
* Whether this store would like to save its data
Expand Down Expand Up @@ -203,7 +207,7 @@ IndexedDBStore.prototype.save = function() {
return Promise.resolve();
};

IndexedDBStore.prototype._reallySave = function() {
IndexedDBStore.prototype._reallySave = degradable(function() {
this._syncTs = Date.now(); // set now to guard against multi-writes

// work out changed users (this doesn't handle deletions but you
Expand All @@ -219,14 +223,12 @@ IndexedDBStore.prototype._reallySave = function() {
this._userModifiedMap[u.userId] = u.getLastModifiedTime();
}

return this.backend.syncToDatabase(userTuples).catch((err) => {
console.error("sync fail:", err);
});
};
return this.backend.syncToDatabase(userTuples);
});

IndexedDBStore.prototype.setSyncData = function(syncData) {
IndexedDBStore.prototype.setSyncData = degradable(function(syncData) {
return this.backend.setSyncData(syncData);
};
}, "setSyncData");

/**
* Returns the out-of-band membership events for this room that
Expand All @@ -235,9 +237,9 @@ IndexedDBStore.prototype.setSyncData = function(syncData) {
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
IndexedDBStore.prototype.getOutOfBandMembers = function(roomId) {
IndexedDBStore.prototype.getOutOfBandMembers = degradable(function(roomId) {
return this.backend.getOutOfBandMembers(roomId);
};
}, "getOutOfBandMembers");

/**
* Stores the out-of-band membership events for this room. Note that
Expand All @@ -247,20 +249,71 @@ IndexedDBStore.prototype.getOutOfBandMembers = function(roomId) {
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
IndexedDBStore.prototype.setOutOfBandMembers = function(roomId, membershipEvents) {
IndexedDBStore.prototype.setOutOfBandMembers = degradable(function(
roomId,
membershipEvents,
) {
MemoryStore.prototype.setOutOfBandMembers.call(this, roomId, membershipEvents);
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
};
}, "setOutOfBandMembers");

IndexedDBStore.prototype.clearOutOfBandMembers = function(roomId) {
IndexedDBStore.prototype.clearOutOfBandMembers = degradable(function(roomId) {
MemoryStore.prototype.clearOutOfBandMembers.call(this);
return this.backend.clearOutOfBandMembers(roomId);
};
}, "clearOutOfBandMembers");

IndexedDBStore.prototype.getClientOptions = function() {
IndexedDBStore.prototype.getClientOptions = degradable(function() {
return this.backend.getClientOptions();
};
}, "getClientOptions");

IndexedDBStore.prototype.storeClientOptions = function(options) {
IndexedDBStore.prototype.storeClientOptions = degradable(function(options) {
MemoryStore.prototype.storeClientOptions.call(this, options);
return this.backend.storeClientOptions(options);
};
}, "storeClientOptions");

module.exports.IndexedDBStore = IndexedDBStore;

/**
* All member functions of `IndexedDBStore` that access the backend use this wrapper to
* watch for failures after initial store startup, including `QuotaExceededError` as
* free disk space changes, etc.
*
* When IndexedDB fails via any of these paths, we degrade this back to a `MemoryStore`
* in place so that the current operation and all future ones are in-memory only.
*
* @param {Function} func The degradable work to do.
* @param {String} fallback The method name for fallback.
* @returns {Function} A wrapped member function.
*/
function degradable(func, fallback) {
return async function(...args) {
try {
return await func.call(this, ...args);
} catch (e) {
console.error("IndexedDBStore failure, degrading to MemoryStore", e);
this.emit("degraded", e);
try {
// We try to delete IndexedDB after degrading since this store is only a
// cache (the app will still function correctly without the data).
// It's possible that deleting repair IndexedDB for the next app load,
// potenially by making a little more space available.
console.log("IndexedDBStore trying to delete degraded data");
await this.backend.clearDatabase();
console.log("IndexedDBStore delete after degrading succeeeded");
} catch (e) {
console.warn("IndexedDBStore delete after degrading failed", e);
}
// Degrade the store from being an instance of `IndexedDBStore` to instead be
// an instance of `MemoryStore` so that future API calls use the memory path
// directly and skip IndexedDB entirely. This should be safe as
// `IndexedDBStore` already extends from `MemoryStore`, so we are making the
// store become its parent type in a way. The mutator methods of
// `IndexedDBStore` also maintain the state that `MemoryStore` uses (many are
// not overridden at all).
Object.setPrototypeOf(this, MemoryStore.prototype);
if (fallback) {
return await MemoryStore.prototype[fallback].call(this, ...args);
}
}
};
}
7 changes: 7 additions & 0 deletions src/store/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ module.exports.MemoryStore.prototype = {
};
return Promise.resolve();
},

/**
* Returns the out-of-band membership events for this room that
* were previously loaded.
Expand All @@ -395,6 +396,7 @@ module.exports.MemoryStore.prototype = {
getOutOfBandMembers: function(roomId) {
return Promise.resolve(this._oobMembers[roomId] || null);
},

/**
* Stores the out-of-band membership events for this room. Note that
* it still makes sense to store an empty array as the OOB status for the room is
Expand All @@ -408,6 +410,11 @@ module.exports.MemoryStore.prototype = {
return Promise.resolve();
},

clearOutOfBandMembers: function() {
this._oobMembers = {};
return Promise.resolve();
},

getClientOptions: function() {
return Promise.resolve(this._clientOptions);
},
Expand Down

0 comments on commit b1b4941

Please sign in to comment.