Bug 1658597 - Destroy local DB when synchronization is broken r=barret
Differential Revision: https://phabricator.services.mozilla.com/D147755
This commit is contained in:
@@ -26,6 +26,10 @@ var EXPORTED_SYMBOLS = ["Database"];
|
|||||||
* (with the objective of getting rid of kinto-offline-client)
|
* (with the objective of getting rid of kinto-offline-client)
|
||||||
*/
|
*/
|
||||||
class Database {
|
class Database {
|
||||||
|
static destroy() {
|
||||||
|
return destroyIDB();
|
||||||
|
}
|
||||||
|
|
||||||
constructor(identifier) {
|
constructor(identifier) {
|
||||||
ensureShutdownBlocker();
|
ensureShutdownBlocker();
|
||||||
this.identifier = identifier;
|
this.identifier = identifier;
|
||||||
@@ -405,6 +409,35 @@ async function executeIDB(storeNames, callback, options = {}) {
|
|||||||
return promise.finally(finishedFn);
|
return promise.finally(finishedFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function destroyIDB() {
|
||||||
|
if (gDB) {
|
||||||
|
if (gShutdownStarted || Services.startup.shuttingDown) {
|
||||||
|
throw new lazy.IDBHelpers.ShutdownError(
|
||||||
|
"The application is shutting down",
|
||||||
|
"destroyIDB()"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will return immediately; the actual close will happen once
|
||||||
|
// there are no more running transactions.
|
||||||
|
gDB.close();
|
||||||
|
const allTransactions = new Set([
|
||||||
|
...gPendingWriteOperations,
|
||||||
|
...gPendingReadOnlyTransactions,
|
||||||
|
]);
|
||||||
|
for (let transaction of Array.from(allTransactions)) {
|
||||||
|
try {
|
||||||
|
transaction.abort();
|
||||||
|
} catch (ex) {
|
||||||
|
// Ignore errors to abort transactions, we'll destroy everything.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gDB = null;
|
||||||
|
gDBPromise = null;
|
||||||
|
return lazy.IDBHelpers.destroyIDB();
|
||||||
|
}
|
||||||
|
|
||||||
function makeNestedObjectFromArr(arr, val, nestedFiltersObj) {
|
function makeNestedObjectFromArr(arr, val, nestedFiltersObj) {
|
||||||
const last = arr.length - 1;
|
const last = arr.length - 1;
|
||||||
return arr.reduce((acc, cv, i) => {
|
return arr.reduce((acc, cv, i) => {
|
||||||
|
|||||||
@@ -202,10 +202,19 @@ async function openIDB(allowUpgrades = true) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function destroyIDB() {
|
||||||
|
const request = indexedDB.deleteDatabase(DB_NAME);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request.onerror = event => reject(new IndexedDBError(event.target.error));
|
||||||
|
request.onsuccess = () => resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var IDBHelpers = {
|
var IDBHelpers = {
|
||||||
bulkOperationHelper,
|
bulkOperationHelper,
|
||||||
executeIDB,
|
executeIDB,
|
||||||
openIDB,
|
openIDB,
|
||||||
|
destroyIDB,
|
||||||
IndexedDBError,
|
IndexedDBError,
|
||||||
ShutdownError,
|
ShutdownError,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -93,6 +93,14 @@ class SyncHistory {
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the most recent entry.
|
||||||
|
*/
|
||||||
|
async last() {
|
||||||
|
// List is sorted from newer to older.
|
||||||
|
return (await this.list())[0];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wipe out the **whole** store.
|
* Wipe out the **whole** store.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ XPCOMUtils.defineLazyModuleGetters(lazy, {
|
|||||||
pushBroadcastService: "resource://gre/modules/PushBroadcastService.jsm",
|
pushBroadcastService: "resource://gre/modules/PushBroadcastService.jsm",
|
||||||
RemoteSettingsClient: "resource://services-settings/RemoteSettingsClient.jsm",
|
RemoteSettingsClient: "resource://services-settings/RemoteSettingsClient.jsm",
|
||||||
SyncHistory: "resource://services-settings/SyncHistory.jsm",
|
SyncHistory: "resource://services-settings/SyncHistory.jsm",
|
||||||
|
Database: "resource://services-settings/Database.jsm",
|
||||||
Utils: "resource://services-settings/Utils.jsm",
|
Utils: "resource://services-settings/Utils.jsm",
|
||||||
FilterExpressions:
|
FilterExpressions:
|
||||||
"resource://gre/modules/components-utils/FilterExpressions.jsm",
|
"resource://gre/modules/components-utils/FilterExpressions.jsm",
|
||||||
@@ -73,6 +74,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
|||||||
10
|
10
|
||||||
);
|
);
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
|
lazy,
|
||||||
|
"gPrefDestroyBrokenEnabled",
|
||||||
|
PREF_SETTINGS_BRANCH + "destroy_broken_db_enabled",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default entry filtering function, in charge of excluding remote settings entries
|
* Default entry filtering function, in charge of excluding remote settings entries
|
||||||
* where the JEXL expression evaluates into a falsy value.
|
* where the JEXL expression evaluates into a falsy value.
|
||||||
@@ -251,6 +259,38 @@ function remoteSettingsFunction() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When triggered from the daily timer, we try to recover a broken
|
||||||
|
// sync state by destroying the local DB completely and retrying from scratch.
|
||||||
|
if (
|
||||||
|
lazy.gPrefDestroyBrokenEnabled &&
|
||||||
|
trigger == "timer" &&
|
||||||
|
(await isSynchronizationBroken())
|
||||||
|
) {
|
||||||
|
// We don't want to destroy the local DB if the failures are related to
|
||||||
|
// network or server errors though.
|
||||||
|
const lastStatus = await lazy.gSyncHistory.last();
|
||||||
|
const lastErrorClass =
|
||||||
|
lazy.RemoteSettingsClient[lastStatus?.infos?.errorName] || Error;
|
||||||
|
const isLocalError = !(
|
||||||
|
lastErrorClass.prototype instanceof lazy.RemoteSettingsClient.APIError
|
||||||
|
);
|
||||||
|
if (isLocalError) {
|
||||||
|
console.warn(
|
||||||
|
"Synchronization has failed consistently. Destroy database."
|
||||||
|
);
|
||||||
|
// Clear the last ETag to refetch everything.
|
||||||
|
lazy.gPrefs.clearUserPref(PREF_SETTINGS_LAST_ETAG);
|
||||||
|
// Clear the history, to avoid re-destroying several times in a row.
|
||||||
|
await lazy.gSyncHistory.clear().catch(error => Cu.reportError(error));
|
||||||
|
// Delete the whole IndexedDB database.
|
||||||
|
await lazy.Database.destroy().catch(error => Cu.reportError(error));
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`Synchronization is broken, but last error is ${lastStatus}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lazy.console.info("Start polling for changes");
|
lazy.console.info("Start polling for changes");
|
||||||
Services.obs.notifyObservers(
|
Services.obs.notifyObservers(
|
||||||
null,
|
null,
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
/* import-globals-from ../../../common/tests/unit/head_helpers.js */
|
||||||
|
|
||||||
|
const { AppConstants } = ChromeUtils.import(
|
||||||
|
"resource://gre/modules/AppConstants.jsm"
|
||||||
|
);
|
||||||
|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||||
|
|
||||||
|
const { SyncHistory } = ChromeUtils.import(
|
||||||
|
"resource://services-settings/SyncHistory.jsm"
|
||||||
|
);
|
||||||
|
const { RemoteSettingsClient } = ChromeUtils.import(
|
||||||
|
"resource://services-settings/RemoteSettingsClient.jsm"
|
||||||
|
);
|
||||||
|
const { RemoteSettings } = ChromeUtils.import(
|
||||||
|
"resource://services-settings/remote-settings.js"
|
||||||
|
);
|
||||||
|
const { Utils } = ChromeUtils.import("resource://services-settings/Utils.jsm");
|
||||||
|
|
||||||
|
const PREF_SETTINGS_SERVER = "services.settings.server";
|
||||||
|
const CHANGES_PATH = "/v1" + Utils.CHANGES_PATH;
|
||||||
|
const BROKEN_SYNC_THRESHOLD = 10; // See default pref value
|
||||||
|
|
||||||
|
let server;
|
||||||
|
let client;
|
||||||
|
let maybeSyncBackup;
|
||||||
|
|
||||||
|
async function clear_state() {
|
||||||
|
// Disable logging output.
|
||||||
|
Services.prefs.setCharPref("services.settings.loglevel", "critical");
|
||||||
|
// Pull data from the test server.
|
||||||
|
Services.prefs.setCharPref(
|
||||||
|
PREF_SETTINGS_SERVER,
|
||||||
|
`http://localhost:${server.identity.primaryPort}/v1`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear sync history.
|
||||||
|
await new SyncHistory("").clear();
|
||||||
|
|
||||||
|
// Simulate a response whose ETag gets incremented on each call
|
||||||
|
// (in order to generate several history entries, indexed by timestamp).
|
||||||
|
let timestamp = 1337;
|
||||||
|
server.registerPathHandler(CHANGES_PATH, (request, response) => {
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
response.setHeader("Date", new Date(1000000).toUTCString());
|
||||||
|
response.setHeader("ETag", `"${timestamp}"`);
|
||||||
|
response.write(
|
||||||
|
JSON.stringify({
|
||||||
|
timestamp,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
last_modified: ++timestamp,
|
||||||
|
bucket: "main",
|
||||||
|
collection: "desktop-manager",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore original maybeSync() method between each test.
|
||||||
|
client.maybeSync = maybeSyncBackup;
|
||||||
|
}
|
||||||
|
|
||||||
|
function run_test() {
|
||||||
|
// Set up an HTTP Server
|
||||||
|
server = new HttpServer();
|
||||||
|
server.start(-1);
|
||||||
|
|
||||||
|
client = RemoteSettings("desktop-manager");
|
||||||
|
maybeSyncBackup = client.maybeSync;
|
||||||
|
|
||||||
|
run_next_test();
|
||||||
|
|
||||||
|
registerCleanupFunction(() => {
|
||||||
|
server.stop(() => {});
|
||||||
|
// Restore original maybeSync() method when test suite is done.
|
||||||
|
client.maybeSync = maybeSyncBackup;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_db_is_destroyed_when_sync_is_broken() {
|
||||||
|
// Simulate a successful sync.
|
||||||
|
client.maybeSync = async () => {
|
||||||
|
// Store some data in local DB.
|
||||||
|
await client.db.importChanges({}, 1515, []);
|
||||||
|
};
|
||||||
|
await RemoteSettings.pollChanges({ trigger: "timer" });
|
||||||
|
|
||||||
|
// Register a client with a failing sync method.
|
||||||
|
client.maybeSync = () => {
|
||||||
|
throw new RemoteSettingsClient.InvalidSignatureError(
|
||||||
|
"main/desktop-manager"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now obtain several failures in a row.
|
||||||
|
for (var i = 0; i < BROKEN_SYNC_THRESHOLD; i++) {
|
||||||
|
try {
|
||||||
|
await RemoteSettings.pollChanges({ trigger: "timer" });
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Synchronization is in broken state.
|
||||||
|
Assert.equal(
|
||||||
|
await client.db.getLastModified(),
|
||||||
|
1515,
|
||||||
|
"Local DB was not destroyed yet"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Synchronize again. Broken state will be detected.
|
||||||
|
try {
|
||||||
|
await RemoteSettings.pollChanges({ trigger: "timer" });
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// DB was destroyed.
|
||||||
|
Assert.equal(
|
||||||
|
await client.db.getLastModified(),
|
||||||
|
null,
|
||||||
|
"Local DB was destroyed"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_db_is_not_destroyed_when_state_is_server_error() {
|
||||||
|
// Since we don't mock the server endpoints to obtain the changeset of this
|
||||||
|
// collection, the call to `maybeSync()` will fail with network errors.
|
||||||
|
|
||||||
|
// Store some data in local DB.
|
||||||
|
await client.db.importChanges({}, 1515, []);
|
||||||
|
|
||||||
|
// Now obtain several failures in a row.
|
||||||
|
let lastError;
|
||||||
|
for (var i = 0; i < BROKEN_SYNC_THRESHOLD + 1; i++) {
|
||||||
|
try {
|
||||||
|
await RemoteSettings.pollChanges({ trigger: "timer" });
|
||||||
|
} catch (e) {
|
||||||
|
lastError = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assert.ok(
|
||||||
|
/Cannot parse server content/.test(lastError.message),
|
||||||
|
"Error is about server"
|
||||||
|
);
|
||||||
|
// DB was not destroyed.
|
||||||
|
Assert.equal(
|
||||||
|
await client.db.getLastModified(),
|
||||||
|
1515,
|
||||||
|
"Local DB was not destroyed"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(clear_state);
|
||||||
@@ -12,6 +12,7 @@ support-files = test_attachments_downloader/**
|
|||||||
[test_remote_settings_dump_lastmodified.js]
|
[test_remote_settings_dump_lastmodified.js]
|
||||||
[test_remote_settings_offline.js]
|
[test_remote_settings_offline.js]
|
||||||
[test_remote_settings_poll.js]
|
[test_remote_settings_poll.js]
|
||||||
|
[test_remote_settings_recover_broken.js]
|
||||||
[test_remote_settings_worker.js]
|
[test_remote_settings_worker.js]
|
||||||
[test_remote_settings_jexl_filters.js]
|
[test_remote_settings_jexl_filters.js]
|
||||||
[test_remote_settings_release_prefs.js]
|
[test_remote_settings_release_prefs.js]
|
||||||
|
|||||||
Reference in New Issue
Block a user