Bug 1317223 (part 1) - a collection_repair module (without any repairers) and integration with the clients engine. r=rnewman

This creates a collection_repair module, somewhat analogous to the existing
collection_validator module. This defines the public interface to request a
new repair and respond to a remote repair request, and also includes changes
to clients.js to call this public interface.

MozReview-Commit-ID: 9JPpRrLgFoR
This commit is contained in:
Mark Hammond 2017-03-02 16:14:51 +11:00
Родитель 55139d4741
Коммит fdeace22db
4 изменённых файлов: 210 добавлений и 7 удалений

Просмотреть файл

@ -0,0 +1,123 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-sync/main.js");
this.EXPORTED_SYMBOLS = ["getRepairRequestor", "getAllRepairRequestors",
"CollectionRepairRequestor",
"getRepairResponder",
"CollectionRepairResponder"];
// The individual requestors/responders, lazily loaded.
const REQUESTORS = {
}
const RESPONDERS = {
}
// Should we maybe enforce the requestors being a singleton?
function _getRepairConstructor(which, collection) {
if (!(collection in which)) {
return null;
}
let [modname, symbolname] = which[collection];
let ns = {};
Cu.import("resource://services-sync/" + modname, ns);
return ns[symbolname];
}
function getRepairRequestor(collection) {
let ctor = _getRepairConstructor(REQUESTORS, collection);
if (!ctor) {
return null;
}
return new ctor();
}
function getAllRepairRequestors() {
let result = {};
for (let collection of Object.keys(REQUESTORS)) {
let ctor = _getRepairConstructor(REQUESTORS, collection);
result[collection] = new ctor();
}
return result;
}
function getRepairResponder(collection) {
let ctor = _getRepairConstructor(RESPONDERS, collection);
if (!ctor) {
return null;
}
return new ctor();
}
// The abstract classes.
class CollectionRepairRequestor {
constructor(service = null) {
// allow service to be mocked in tests.
this.service = service || Weave.Service;
}
/* See if the repairer is willing and able to begin a repair process given
the specified validation information.
Returns true if a repair was started and false otherwise.
@param validationInfo {Object}
The validation info as returned by the collection's validator.
@param flowID {String}
A guid that uniquely identifies this repair process for this
collection, and which should be sent to any requestors and
reported in telemetry.
*/
startRepairs(validationInfo, flowID) {
throw new Error("not implemented");
}
/* Work out what state our current repair request is in, and whether it can
proceed to a new state.
Returns true if we could continue the repair - even if the state didn't
actually move. Returns false if we aren't actually repairing.
@param responseInfo {Object}
An optional response to a previous repair request, as returned
by a remote repair responder.
*/
continueRepairs(responseInfo = null) {
throw new Error("not implemented");
}
}
class CollectionRepairResponder {
constructor(service = null) {
// allow service to be mocked in tests.
this.service = service || Weave.Service;
}
/* Take some action in response to a repair request. Returns a promise that
resolves once the repair process has started, or rejects if there
was an error starting the repair.
Note that when the promise resolves the repair is not yet complete - at
some point in the future the repair will auto-complete, at which time
|rawCommand| will be removed from the list of client commands for this
client.
@param request {Object}
The repair request as sent by another client.
@param rawCommand {Object}
The command object as stored in the clients engine, and which
will be automatically removed once a repair completes.
*/
async repair(request, rawCommand) {
throw new Error("not implemented");
}
}

Просмотреть файл

@ -39,6 +39,12 @@ Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "getRepairRequestor",
"resource://services-sync/collection_repair.js");
XPCOMUtils.defineLazyModuleGetter(this, "getRepairResponder",
"resource://services-sync/collection_repair.js");
const CLIENTS_TTL = 1814400; // 21 days
const CLIENTS_TTL_REFRESH = 604800; // 7 days
const STALE_CLIENT_REMOTE_AGE = 604800; // 7 days
@ -100,9 +106,13 @@ ClientEngine.prototype = {
return Object.values(this._store._remoteClients).filter(v => !v.stale);
},
remoteClientExists(id) {
remoteClient(id) {
let client = this._store._remoteClients[id];
return !!(client && !client.stale);
return client && !client.stale ? client : null;
},
remoteClientExists(id) {
return !!this.remoteClient(id);
},
// Aggregate some stats on the composition of clients on this account
@ -250,6 +260,20 @@ ClientEngine.prototype = {
);
},
// Gets commands for a client we are yet to write to the server. Doesn't
// include commands for that client which are already on the server.
// We should rename this!
getClientCommands(clientId) {
const allCommands = this._readCommands();
return allCommands[clientId] || [];
},
removeLocalCommand(command) {
// the implementation of this engine is such that adding a command to
// the local client is how commands are deleted! ¯\_(ツ)_/¯
this._addClientCommand(this.localID, command);
},
_addClientCommand(clientId, command) {
const allCommands = this._readCommands();
const clientCommands = allCommands[clientId] || [];
@ -489,6 +513,8 @@ ClientEngine.prototype = {
wipeEngine: { args: 1, desc: "Delete all client data for engine" },
logout: { args: 0, desc: "Log out client" },
displayURI: { args: 3, desc: "Instruct a client to display a URI" },
repairRequest: {args: 1, desc: "Instruct a client to initiate a repair"},
repairResponse: {args: 1, desc: "Instruct a client a repair request is complete"},
},
/**
@ -543,12 +569,13 @@ ClientEngine.prototype = {
const clearedCommands = this._readCommands()[this.localID];
const commands = this.localCommands.filter(command => !hasDupeCommand(clearedCommands, command));
let didRemoveCommand = false;
let URIsToDisplay = [];
// Process each command in order.
for (let rawCommand of commands) {
let shouldRemoveCommand = true; // most commands are auto-removed.
let {command, args, flowID} = rawCommand;
this._log.debug("Processing command: " + command + "(" + args + ")");
this._log.debug("Processing command " + command, args);
this.service.recordTelemetryEvent("processcommand", command, undefined,
{ flowID });
@ -574,14 +601,65 @@ ClientEngine.prototype = {
let [uri, clientId, title] = args;
URIsToDisplay.push({ uri, clientId, title });
break;
case "repairResponse": {
// When we send a repair request to another device that understands
// it, that device will send a response indicating what it did.
let response = args[0];
let requestor = getRepairRequestor(response.collection);
if (!requestor) {
this._log.warn("repairResponse for unknown collection", response);
break;
}
if (!requestor.continueRepairs(response)) {
this._log.warn("repairResponse couldn't continue the repair", response);
}
break;
}
case "repairRequest": {
// Another device has sent us a request to make some repair.
let request = args[0];
let responder = getRepairResponder(request.collection);
if (!responder) {
this._log.warn("repairRequest for unknown collection", request);
break;
}
try {
if (Async.promiseSpinningly(responder.repair(request, rawCommand))) {
// We've started a repair - once that collection has synced it
// will write a "response" command and arrange for this repair
// request to be removed from the local command list - if we
// removed it now we might fail to write a response in cases of
// premature shutdown etc.
shouldRemoveCommand = false;
}
} catch (ex) {
if (Async.isShutdownException(ex)) {
// Let's assume this error was caused by the shutdown, so let
// it try again next time.
throw ex;
}
// otherwise there are no second chances - the command is removed
// and will not be tried again.
// (Note that this shouldn't be hit in the normal case - it's
// expected the responder will handle all reasonable failures and
// write a response indicating that it couldn't do what was asked.)
this._log.error("Failed to handle a repair request", ex);
}
break;
}
default:
this._log.debug("Received an unknown command: " + command);
this._log.warn("Received an unknown command: " + command);
break;
}
// Add the command to the "cleared" commands list
this._addClientCommand(this.localID, rawCommand)
if (shouldRemoveCommand) {
this.removeLocalCommand(rawCommand);
didRemoveCommand = true;
}
}
if (didRemoveCommand) {
this._tracker.addChangedID(this.localID);
}
this._tracker.addChangedID(this.localID);
if (URIsToDisplay.length) {
this._handleDisplayURIs(URIsToDisplay);

Просмотреть файл

@ -21,6 +21,7 @@ EXTRA_JS_MODULES['services-sync'] += [
'modules/addonutils.js',
'modules/bookmark_validator.js',
'modules/browserid_identity.js',
'modules/collection_repair.js',
'modules/collection_validator.js',
'modules/engines.js',
'modules/keys.js',

Просмотреть файл

@ -35,6 +35,7 @@
"CloudSyncLocal.jsm": ["Local"],
"CloudSyncPlacesWrapper.jsm": ["PlacesWrapper"],
"CloudSyncTabs.jsm": ["Tabs"],
"collection_repair.js": ["getRepairRequestor", "getAllRepairRequestors", "CollectionRepairRequestor", "getRepairResponder", "CollectionRepairResponder"],
"collection_validator.js": ["CollectionValidator", "CollectionProblemData"],
"Console.jsm": ["console", "ConsoleAPI"],
"constants.js": ["WEAVE_VERSION", "SYNC_API_VERSION", "USER_API_VERSION", "MISC_API_VERSION", "STORAGE_VERSION", "PREFS_BRANCH", "PWDMGR_HOST", "PWDMGR_PASSWORD_REALM", "PWDMGR_PASSPHRASE_REALM", "PWDMGR_KEYBUNDLE_REALM", "DEFAULT_KEYBUNDLE_NAME", "HMAC_INPUT", "SYNC_KEY_ENCODED_LENGTH", "SYNC_KEY_DECODED_LENGTH", "SYNC_KEY_HYPHENATED_LENGTH", "NO_SYNC_NODE_INTERVAL", "MAX_ERROR_COUNT_BEFORE_BACKOFF", "MAX_IGNORE_ERROR_COUNT", "MINIMUM_BACKOFF_INTERVAL", "MAXIMUM_BACKOFF_INTERVAL", "HMAC_EVENT_INTERVAL", "MASTER_PASSWORD_LOCKED_RETRY_INTERVAL", "DEFAULT_BLOCK_PERIOD", "MOBILE_BATCH_SIZE", "DEFAULT_GUID_FETCH_BATCH_SIZE", "DEFAULT_MOBILE_GUID_FETCH_BATCH_SIZE", "DEFAULT_STORE_BATCH_SIZE", "HISTORY_STORE_BATCH_SIZE", "FORMS_STORE_BATCH_SIZE", "PASSWORDS_STORE_BATCH_SIZE", "ADDONS_STORE_BATCH_SIZE", "APPS_STORE_BATCH_SIZE", "DEFAULT_DOWNLOAD_BATCH_SIZE", "SINGLE_USER_THRESHOLD", "MULTI_DEVICE_THRESHOLD", "SCORE_INCREMENT_SMALL", "SCORE_INCREMENT_MEDIUM", "SCORE_INCREMENT_XLARGE", "SCORE_UPDATE_DELAY", "IDLE_OBSERVER_BACK_DELAY", "MAX_UPLOAD_RECORDS", "MAX_UPLOAD_BYTES", "MAX_HISTORY_UPLOAD", "MAX_HISTORY_DOWNLOAD", "NOTIFY_TAB_SENT_TTL_SECS", "STATUS_OK", "SYNC_FAILED", "LOGIN_FAILED", "SYNC_FAILED_PARTIAL", "CLIENT_NOT_CONFIGURED", "STATUS_DISABLED", "MASTER_PASSWORD_LOCKED", "LOGIN_SUCCEEDED", "SYNC_SUCCEEDED", "ENGINE_SUCCEEDED", "LOGIN_FAILED_NO_USERNAME", "LOGIN_FAILED_NO_PASSWORD", "LOGIN_FAILED_NO_PASSPHRASE", "LOGIN_FAILED_NETWORK_ERROR", "LOGIN_FAILED_SERVER_ERROR", "LOGIN_FAILED_INVALID_PASSPHRASE", "LOGIN_FAILED_LOGIN_REJECTED", "METARECORD_DOWNLOAD_FAIL", "VERSION_OUT_OF_DATE", "DESKTOP_VERSION_OUT_OF_DATE", "SETUP_FAILED_NO_PASSPHRASE", "CREDENTIALS_CHANGED", "ABORT_SYNC_COMMAND", "NO_SYNC_NODE_FOUND", "OVER_QUOTA", "PROLONGED_SYNC_FAILURE", "SERVER_MAINTENANCE", "RESPONSE_OVER_QUOTA", "ENGINE_UPLOAD_FAIL", "ENGINE_DOWNLOAD_FAIL", "ENGINE_UNKNOWN_FAIL", "ENGINE_APPLY_FAIL", "ENGINE_METARECORD_DOWNLOAD_FAIL", "ENGINE_METARECORD_UPLOAD_FAIL", "ENGINE_BATCH_INTERRUPTED", "JPAKE_ERROR_CHANNEL", "JPAKE_ERROR_NETWORK", "JPAKE_ERROR_SERVER", "JPAKE_ERROR_TIMEOUT", "JPAKE_ERROR_INTERNAL", "JPAKE_ERROR_INVALID", "JPAKE_ERROR_NODATA", "JPAKE_ERROR_KEYMISMATCH", "JPAKE_ERROR_WRONGMESSAGE", "JPAKE_ERROR_USERABORT", "JPAKE_ERROR_DELAYUNSUPPORTED", "INFO_COLLECTIONS", "INFO_COLLECTION_USAGE", "INFO_COLLECTION_COUNTS", "INFO_QUOTA", "kSyncMasterPasswordLocked", "kSyncWeaveDisabled", "kSyncNetworkOffline", "kSyncBackoffNotMet", "kFirstSyncChoiceNotMade", "FIREFOX_ID", "FENNEC_ID", "SEAMONKEY_ID", "TEST_HARNESS_ID", "MIN_PP_LENGTH", "MIN_PASS_LENGTH", "DEVICE_TYPE_DESKTOP", "DEVICE_TYPE_MOBILE", "SQLITE_MAX_VARIABLE_NUMBER"],