Bug 1901526 - Build out backup scheduling mechanism. r=backup-reviewers,kpatenio,sthompson

Differential Revision: https://phabricator.services.mozilla.com/D213468
This commit is contained in:
Mike Conley 2024-06-27 17:27:27 +00:00
Родитель 5dc35d19c8
Коммит 16715733d3
6 изменённых файлов: 551 добавлений и 2 удалений

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

@ -3119,6 +3119,8 @@ pref("browser.backup.preferences.ui.enabled", false);
pref("browser.backup.sqlite.pages_per_step", 5);
// The delay between SQLite database backup steps in milliseconds.
pref("browser.backup.sqlite.step_delay_ms", 250);
pref("browser.backup.scheduled.idle-threshold-seconds", 300);
pref("browser.backup.scheduled.minimum-time-between-backups-seconds", 3600);
// Pref to enable the new profiles
pref("browser.profiles.enabled", false);

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

@ -15,6 +15,13 @@ import {
const BACKUP_DIR_PREF_NAME = "browser.backup.location";
const SCHEDULED_BACKUPS_ENABLED_PREF_NAME = "browser.backup.scheduled.enabled";
const IDLE_THRESHOLD_SECONDS_PREF_NAME =
"browser.backup.scheduled.idle-threshold-seconds";
const MINIMUM_TIME_BETWEEN_BACKUPS_SECONDS_PREF_NAME =
"browser.backup.scheduled.minimum-time-between-backups-seconds";
const LAST_BACKUP_TIMESTAMP_PREF_NAME =
"browser.backup.scheduled.last-backup-timestamp";
const SCHEMAS = Object.freeze({
BACKUP_MANIFEST: 1,
ARCHIVE_JSON_BLOCK: 2,
@ -121,6 +128,20 @@ XPCOMUtils.defineLazyPreferenceGetter(
}
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"minimumTimeBetweenBackupsSeconds",
MINIMUM_TIME_BETWEEN_BACKUPS_SECONDS_PREF_NAME,
3600 /* 1 hour */
);
XPCOMUtils.defineLazyServiceGetter(
lazy,
"idleService",
"@mozilla.org/widget/useridleservice;1",
"nsIUserIdleService"
);
/**
* A class that wraps a multipart/mixed stream converter instance, and streams
* in the binary part of a single-file archive (which should be at the second
@ -542,6 +563,8 @@ export class BackupService extends EventTarget {
backupInProgress: false,
scheduledBackupsEnabled: lazy.scheduledBackupsPref,
encryptionEnabled: false,
/** @type {number?} Number of seconds since UNIX epoch */
lastBackupDate: null,
};
/**
@ -756,6 +779,7 @@ export class BackupService extends EventTarget {
this.#instance.takeMeasurements();
});
this.#instance.initBackupScheduler();
return this.#instance;
}
@ -1006,6 +1030,10 @@ export class BackupService extends EventTarget {
manifest.meta
);
let nowSeconds = Math.floor(Date.now() / 1000);
Services.prefs.setIntPref(LAST_BACKUP_TIMESTAMP_PREF_NAME, nowSeconds);
this.#_state.lastBackupDate = nowSeconds;
return {
stagingPath: renamedStagingPath,
compressedStagingPath,
@ -2495,4 +2523,203 @@ export class BackupService extends EventTarget {
this.#_state.encryptionEnabled = false;
this.stateUpdate();
}
/**
* The value of IDLE_THRESHOLD_SECONDS_PREF_NAME at the time that
* initBackupScheduler was called. This is recorded so that if the preference
* changes at runtime, that we properly remove the idle observer in
* uninitBackupScheduler, since it's mapped to the idle time value.
*
* @see BackupService.initBackupScheduler()
* @see BackupService.uninitBackupScheduler()
* @type {number}
*/
#idleThresholdSeconds = null;
/**
* An ES6 class that extends EventTarget cannot, apparently, be coerced into
* a nsIObserver, even when we define QueryInterface. We work around this
* limitation by having the observer be a function that we define at
* registration time. We hold a reference to the observer so that we can
* properly unregister.
*
* @see BackupService.initBackupScheduler()
* @type {Function}
*/
#observer = null;
/**
* True if the backup scheduler system has been initted via
* initBackupScheduler().
*
* @see BackupService.initBackupScheduler()
* @type {boolean}
*/
#backupSchedulerInitted = false;
/**
* Initializes the backup scheduling system. This should be done shortly
* after startup. It is exposed as a public method mainly for ease in testing.
*
* The scheduler will automatically uninitialize itself on the
* quit-application-granted observer notification.
*
* @returns {Promise<undefined>}
*/
async initBackupScheduler() {
if (this.#backupSchedulerInitted) {
lazy.logConsole.warn(
"BackupService scheduler already initting or initted."
);
return;
}
this.#backupSchedulerInitted = true;
let lastBackupPrefValue = Services.prefs.getIntPref(
LAST_BACKUP_TIMESTAMP_PREF_NAME,
0
);
if (!lastBackupPrefValue) {
this.#_state.lastBackupDate = null;
} else {
this.#_state.lastBackupDate = lastBackupPrefValue;
}
this.stateUpdate();
// We'll default to 5 minutes of idle time unless otherwise configured.
const FIVE_MINUTES_IN_SECONDS = 5 * 60;
this.#idleThresholdSeconds = Services.prefs.getIntPref(
IDLE_THRESHOLD_SECONDS_PREF_NAME,
FIVE_MINUTES_IN_SECONDS
);
this.#observer = (subject, topic, data) => {
this.onObserve(subject, topic, data);
};
lazy.logConsole.debug(
`Registering idle observer for ${
this.#idleThresholdSeconds
} seconds of idle time`
);
lazy.idleService.addIdleObserver(
this.#observer,
this.#idleThresholdSeconds
);
lazy.logConsole.debug("Idle observer registered.");
Services.obs.addObserver(this.#observer, "quit-application-granted");
}
/**
* Uninitializes the backup scheduling system.
*
* @returns {Promise<undefined>}
*/
async uninitBackupScheduler() {
if (!this.#backupSchedulerInitted) {
lazy.logConsole.warn(
"Tried to uninitBackupScheduler when it wasn't yet enabled."
);
return;
}
lazy.idleService.removeIdleObserver(
this.#observer,
this.#idleThresholdSeconds
);
Services.obs.removeObserver(this.#observer, "quit-application-granted");
this.#observer = null;
}
/**
* Called by this.#observer on idle from the nsIUserIdleService or
* quit-application-granted from the nsIObserverService. Exposed as a public
* method mainly for ease in testing.
*
* @param {nsISupports|null} _subject
* The nsIUserIdleService for the idle notification, and null for the
* quit-application-granted topic.
* @param {string} topic
* The topic that the notification belongs to.
*/
onObserve(_subject, topic) {
switch (topic) {
case "idle": {
this.onIdle();
break;
}
case "quit-application-granted": {
this.uninitBackupScheduler();
break;
}
}
}
/**
* Called when the nsIUserIdleService reports that user input events have
* not been sent to the application for at least
* IDLE_THRESHOLD_SECONDS_PREF_NAME seconds.
*/
onIdle() {
lazy.logConsole.debug("Saw idle callback");
if (lazy.scheduledBackupsPref) {
lazy.logConsole.debug("Scheduled backups enabled.");
let now = Math.floor(Date.now() / 1000);
let lastBackupDate = this.#_state.lastBackupDate;
if (lastBackupDate && lastBackupDate > now) {
lazy.logConsole.error(
"Last backup was somehow in the future. Resetting the preference."
);
lastBackupDate = null;
this.#_state.lastBackupDate = null;
this.stateUpdate();
}
if (!lastBackupDate) {
lazy.logConsole.debug("No last backup time recorded in prefs.");
} else {
lazy.logConsole.debug(
"Last backup was: ",
new Date(lastBackupDate * 1000)
);
}
if (
!lastBackupDate ||
now - lastBackupDate > lazy.minimumTimeBetweenBackupsSeconds
) {
lazy.logConsole.debug(
"Last backup exceeded minimum time between backups. Queing a " +
"backup via idleDispatch."
);
// Just because the user hasn't sent us events in a while doesn't mean
// that the browser itself isn't busy. It might be, for example, playing
// video or doing a complex calculation that the user is actively
// waiting to complete, and we don't want to draw resources from that.
// Instead, we'll use ChromeUtils.idleDispatch to wait until the event
// loop in the parent process isn't so busy with higher priority things.
this.createBackupOnIdleDispatch();
} else {
lazy.logConsole.debug(
"Last backup was too recent. Not creating one for now."
);
}
}
}
/**
* Calls BackupService.createBackup at the next moment when the event queue
* is not busy with higher priority events. This is intentionally broken out
* into its own method to make it easier to stub out in tests.
*/
createBackupOnIdleDispatch() {
ChromeUtils.idleDispatch(() => {
lazy.logConsole.debug(
"idleDispatch fired. Attempting to create a backup."
);
this.createBackup();
});
}
}

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

@ -115,7 +115,9 @@ async function testCreateBackupHelper(sandbox, taskFn) {
"createBackupTest"
);
Assert.ok(!bs.state.lastBackupDate, "No backup date is stored in state.");
await bs.createBackup({ profilePath: fakeProfilePath });
Assert.ok(bs.state.lastBackupDate, "The backup date was recorded.");
// We expect the staging folder to exist then be renamed under the fakeProfilePath.
// We should also find a folder for each fake BackupResource.

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

@ -0,0 +1,300 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { MockRegistrar } = ChromeUtils.importESModule(
"resource://testing-common/MockRegistrar.sys.mjs"
);
const SCHEDULED_BACKUPS_ENABLED_PREF_NAME = "browser.backup.scheduled.enabled";
const IDLE_THRESHOLD_SECONDS_PREF_NAME =
"browser.backup.scheduled.idle-threshold-seconds";
const LAST_BACKUP_TIMESTAMP_PREF_NAME =
"browser.backup.scheduled.last-backup-timestamp";
const MINIMUM_TIME_BETWEEN_BACKUPS_SECONDS_PREF_NAME =
"browser.backup.scheduled.minimum-time-between-backups-seconds";
/**
* This is a very thin nsIUserIdleService implementation that doesn't do much,
* but with sinon we can stub out some parts of it to make sure that the
* BackupService uses it in the way we expect.
*/
let idleService = {
QueryInterface: ChromeUtils.generateQI(["nsIUserIdleService"]),
idleTime: 19999,
disabled: true,
addIdleObserver() {},
removeIdleObserver() {},
};
add_setup(() => {
let fakeIdleServiceCID = MockRegistrar.register(
"@mozilla.org/widget/useridleservice;1",
idleService
);
Services.prefs.setBoolPref(SCHEDULED_BACKUPS_ENABLED_PREF_NAME, true);
// We'll pretend that our threshold between backups is 20 seconds.
Services.prefs.setIntPref(MINIMUM_TIME_BETWEEN_BACKUPS_SECONDS_PREF_NAME, 20);
registerCleanupFunction(() => {
MockRegistrar.unregister(fakeIdleServiceCID);
Services.prefs.clearUserPref(SCHEDULED_BACKUPS_ENABLED_PREF_NAME);
Services.prefs.clearUserPref(
MINIMUM_TIME_BETWEEN_BACKUPS_SECONDS_PREF_NAME
);
});
});
/**
* Tests that calling initBackupScheduler registers a callback with the
* nsIUserIdleService.
*/
add_task(async function test_init_uninitBackupScheduler() {
let bs = new BackupService();
let sandbox = sinon.createSandbox();
sandbox.stub(idleService, "addIdleObserver");
sandbox.stub(idleService, "removeIdleObserver");
await bs.initBackupScheduler();
Assert.ok(
idleService.addIdleObserver.calledOnce,
"addIdleObserver was called"
);
Assert.ok(
idleService.addIdleObserver.firstCall.args[0] instanceof Ci.nsIObserver,
"The first argument to addIdleObserver was an nsIObserver"
);
const THRESHOLD_SECONDS = Services.prefs.getIntPref(
IDLE_THRESHOLD_SECONDS_PREF_NAME
);
Assert.equal(
idleService.addIdleObserver.firstCall.args[1],
THRESHOLD_SECONDS,
"The idle threshold preference value was passed as the second argument."
);
Assert.ok(
idleService.removeIdleObserver.notCalled,
"removeIdleObserver has not been called yet."
);
// Hold a reference to what addIdleObserver was called with as its first
// argument, so we can compare it against what's passed to removeIdleObserver.
let addObserverArg = idleService.addIdleObserver.firstCall.args[0];
// We want to make sure that uninitBackupScheduler doesn't call this again,
// so reset its call history.
idleService.addIdleObserver.resetHistory();
// Now, let's pretend that the preference for the idle threshold changed
// before we could uninit the backup scheduler. We should ensure that this
// change is _not_ reflected whenever deregistration of the idle callback
// occurs, since it wouldn't match the registration arguments.
Services.prefs.setIntPref(
IDLE_THRESHOLD_SECONDS_PREF_NAME,
THRESHOLD_SECONDS + 5
);
bs.uninitBackupScheduler();
Assert.ok(
idleService.addIdleObserver.notCalled,
"addIdleObserver was not called again."
);
Assert.ok(
idleService.removeIdleObserver.calledOnce,
"removeIdleObserver was called once."
);
Assert.ok(
idleService.removeIdleObserver.firstCall.args[0] instanceof Ci.nsIObserver,
"The first argument to addIdleObserver was an nsIObserver"
);
Assert.equal(
idleService.removeIdleObserver.firstCall.args[0],
addObserverArg,
"The first argument to addIdleObserver matches the first argument to removeIdleObserver"
);
Assert.equal(
idleService.removeIdleObserver.firstCall.args[1],
THRESHOLD_SECONDS,
"The original idle threshold preference value was passed as the second argument."
);
sandbox.restore();
Services.prefs.clearUserPref(IDLE_THRESHOLD_SECONDS_PREF_NAME);
});
/**
* Tests that calling BackupService.onObserve with the "idle" notification
* causes the BackupService.onIdle method to be called.
*/
add_task(async function test_BackupService_onObserve_idle() {
let bs = new BackupService();
let sandbox = sinon.createSandbox();
sandbox.stub(bs, "onIdle");
// The subject for the idle notification is always the idle service itself.
bs.onObserve(idleService, "idle");
Assert.ok(bs.onIdle.calledOnce, "BackupService.onIdle was called.");
sandbox.restore();
});
/**
* Tests that calling BackupService.onObserve with the
* "quit-application-granted" notification causes the
* BackupService.uninitBackupScheduler method to be called.
*/
add_task(
async function test_BackupService_onObserve_quit_application_granted() {
let bs = new BackupService();
let sandbox = sinon.createSandbox();
sandbox.stub(bs, "uninitBackupScheduler");
// The subject for the quit-application-granted notification is null.
bs.onObserve(null, "quit-application-granted");
Assert.ok(
bs.uninitBackupScheduler.calledOnce,
"BackupService.uninitBackupScheduler was called."
);
sandbox.restore();
}
);
/**
* Tests that calling onIdle when a backup has never occurred causes a backup to
* get scheduled.
*/
add_task(async function test_BackupService_idle_no_backup_exists() {
// Make sure no last backup timestamp is recorded.
Services.prefs.clearUserPref(LAST_BACKUP_TIMESTAMP_PREF_NAME);
let bs = new BackupService();
let sandbox = sinon.createSandbox();
sandbox.stub(bs, "createBackupOnIdleDispatch");
bs.initBackupScheduler();
Assert.equal(
bs.state.lastBackupDate,
null,
"State should have null for lastBackupDate"
);
bs.onIdle();
Assert.ok(
bs.createBackupOnIdleDispatch.calledOnce,
"BackupService.createBackupOnIdleDispatch was called."
);
sandbox.restore();
});
/**
* Tests that calling onIdle when a backup has occurred recently does not cause
* a backup to get scheduled.
*/
add_task(async function test_BackupService_idle_not_expired_backup() {
// Let's calculate a Date that's five seconds ago.
let fiveSecondsAgo = Date.now() - 5000; /* 5 seconds in milliseconds */
let lastBackupPrefValue = Math.floor(fiveSecondsAgo / 1000);
Services.prefs.setIntPref(
LAST_BACKUP_TIMESTAMP_PREF_NAME,
lastBackupPrefValue
);
let bs = new BackupService();
let sandbox = sinon.createSandbox();
bs.initBackupScheduler();
Assert.equal(
bs.state.lastBackupDate,
lastBackupPrefValue,
"State should have cached lastBackupDate"
);
sandbox.stub(bs, "createBackupOnIdleDispatch");
bs.onIdle();
Assert.ok(
bs.createBackupOnIdleDispatch.notCalled,
"BackupService.createBackupOnIdleDispatch was not called."
);
sandbox.restore();
});
/**
* Tests that calling onIdle when a backup has occurred, but after the threshold
* does cause a backup to get scheduled
*/
add_task(async function test_BackupService_idle_expired_backup() {
// Let's calculate a Date that's twenty five seconds ago.
let twentyFiveSecondsAgo =
Date.now() - 25000; /* 25 seconds in milliseconds */
let lastBackupPrefValue = Math.floor(twentyFiveSecondsAgo / 1000);
Services.prefs.setIntPref(
LAST_BACKUP_TIMESTAMP_PREF_NAME,
lastBackupPrefValue
);
let bs = new BackupService();
let sandbox = sinon.createSandbox();
bs.initBackupScheduler();
Assert.equal(
bs.state.lastBackupDate,
lastBackupPrefValue,
"State should have cached lastBackupDate"
);
sandbox.stub(bs, "createBackupOnIdleDispatch");
bs.onIdle();
Assert.ok(
bs.createBackupOnIdleDispatch.calledOnce,
"BackupService.createBackupOnIdleDispatch was called."
);
sandbox.restore();
});
/**
* Tests that calling onIdle when a backup occurred in the future somehow causes
* a backup to get scheduled.
*/
add_task(async function test_BackupService_idle_time_travel() {
// Let's calculate a Date that's twenty-five seconds in the future.
let twentyFiveSecondsFromNow =
Date.now() + 25000; /* 25 seconds in milliseconds */
let lastBackupPrefValue = Math.floor(twentyFiveSecondsFromNow / 1000);
Services.prefs.setIntPref(
LAST_BACKUP_TIMESTAMP_PREF_NAME,
lastBackupPrefValue
);
let bs = new BackupService();
let sandbox = sinon.createSandbox();
bs.initBackupScheduler();
Assert.equal(
bs.state.lastBackupDate,
lastBackupPrefValue,
"State should have cached lastBackupDate"
);
sandbox.stub(bs, "createBackupOnIdleDispatch");
bs.onIdle();
Assert.ok(
bs.createBackupOnIdleDispatch.calledOnce,
"BackupService.createBackupOnIdleDispatch was called."
);
Assert.equal(
bs.state.lastBackupDate,
null,
"Should have cleared the last backup date."
);
sandbox.restore();
});

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

@ -31,18 +31,20 @@ skip-if = ["apple_silicon && automation"] # bug 1729538
["test_BackupService_renderTemplate.js"]
["test_BackupService_scheduler.js"]
["test_BackupService_schema_versions.js"]
["test_BackupService_takeMeasurements.js"]
["test_MeasurementUtils_fuzzByteSize.js"]
["test_CookiesBackupResource.js"]
["test_CredentialsAndSecurityBackupResource.js"]
["test_FormHistoryBackupResource.js"]
["test_MeasurementUtils_fuzzByteSize.js"]
["test_MiscDataBackupResource.js"]
["test_PlacesBackupResource.js"]

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

@ -3116,6 +3116,22 @@ backupService:
setPref:
branch: user
pref: browser.backup.sqlite.step_delay_ms
idleThresholdSeconds:
description: >-
The number of seconds of user idle time to wait for before considering
to schedule a backup.
type: int
setPref:
branch: user
pref: browser.backup.scheduled.idle-threshold-seconds
minTimeBetweenBackupsSeconds:
description: >-
The minimum number of seconds since the last known backup that must
pass before we might schedule a backup.
type: int
setPref:
branch: user
pref: browser.backup.scheduled.minimum-time-between-backups-seconds
pqcrypto:
description: Prefs that control the use of post-quantum cryptography.