216 lines
5.9 KiB
JavaScript
216 lines
5.9 KiB
JavaScript
/* 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/. */
|
|
|
|
/* The purpose of this class is to limit (cap) sending of DAP reports.
|
|
* The current DAP draft standard is available here:
|
|
* https://github.com/ietf-wg-ppm/draft-ietf-ppm-dap */
|
|
|
|
import { DAPTelemetrySender } from "./DAPTelemetrySender.sys.mjs";
|
|
|
|
const DAY_IN_MILLI = 1000 * 60 * 60 * 24;
|
|
const MINUTE_IN_MILLI = 60 * 1000;
|
|
|
|
let lazy = {};
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
|
|
return console.createInstance({
|
|
prefix: "DAPReportController",
|
|
maxLogLevelPref: "toolkit.telemetry.dap.logLevel",
|
|
});
|
|
});
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
|
IndexedDB: "resource://gre/modules/IndexedDB.sys.mjs",
|
|
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
|
|
});
|
|
|
|
const DB_NAME = "SubmissionCap";
|
|
const DB_VERSION = 1;
|
|
const FREQ_CAP_STORE = "freq_caps";
|
|
const REPORT_STORE = "reports";
|
|
|
|
export class Task {
|
|
constructor({ taskId, vdaf, bits, length, defaultMeasurement }) {
|
|
this._taskId = taskId;
|
|
this._vdaf = vdaf;
|
|
this._bits = bits;
|
|
this._length = length;
|
|
this._defaultMeasurement = defaultMeasurement;
|
|
}
|
|
}
|
|
|
|
export class DAPReportController {
|
|
constructor({ tasks, options, DateNowFn = Date.now } = {}) {
|
|
this._tasks = tasks;
|
|
this._windowDays = options.windowDays;
|
|
this._submissionTimerMins = options.submissionIntervalMins;
|
|
this._timerId = null;
|
|
this._now = DateNowFn;
|
|
}
|
|
|
|
get db() {
|
|
return this._db || (this._db = this.#createOrOpenDb());
|
|
}
|
|
|
|
async #createOrOpenDb() {
|
|
try {
|
|
return await this.#openDatabase();
|
|
} catch {
|
|
throw new Error("DAPVisitCounter unable to load database.");
|
|
}
|
|
}
|
|
|
|
async #openDatabase() {
|
|
return await lazy.IndexedDB.open(DB_NAME, DB_VERSION, db => {
|
|
if (!db.objectStoreNames.contains(FREQ_CAP_STORE)) {
|
|
db.createObjectStore(FREQ_CAP_STORE, { keyPath: "taskId" });
|
|
}
|
|
if (!db.objectStoreNames.contains(REPORT_STORE)) {
|
|
db.createObjectStore(REPORT_STORE, { keyPath: "taskId" });
|
|
}
|
|
});
|
|
}
|
|
|
|
/* Clears a pending report and updates the freq cap data
|
|
*/
|
|
async #releasePendingReport(report) {
|
|
const tx = (await this.db).transaction(
|
|
[REPORT_STORE, FREQ_CAP_STORE],
|
|
"readwrite"
|
|
);
|
|
|
|
const reportStore = tx.objectStore(REPORT_STORE);
|
|
const capStore = tx.objectStore(FREQ_CAP_STORE);
|
|
|
|
await reportStore.delete(report.taskId);
|
|
|
|
let cap = {
|
|
taskId: report.taskId,
|
|
nextReset: this._now() + this._windowDays * DAY_IN_MILLI,
|
|
};
|
|
await capStore.put(cap);
|
|
await tx.done;
|
|
}
|
|
|
|
async getFreqCap(taskId) {
|
|
const tx = (await this.db).transaction(FREQ_CAP_STORE, "readonly");
|
|
const cap = await tx.objectStore(FREQ_CAP_STORE).get(taskId);
|
|
await tx.done;
|
|
return cap;
|
|
}
|
|
|
|
async recordMeasurement(taskId, measurement) {
|
|
try {
|
|
const cap = await this.getFreqCap(taskId);
|
|
|
|
if (cap === undefined || cap.nextReset < this._now()) {
|
|
const taskMetaData = this._tasks[taskId];
|
|
const report = {
|
|
taskId,
|
|
vdaf: taskMetaData._vdaf,
|
|
bits: taskMetaData._bits,
|
|
length: taskMetaData._length,
|
|
measurement,
|
|
};
|
|
const tx = (await this.db).transaction(REPORT_STORE, "readwrite");
|
|
await tx.objectStore(REPORT_STORE).put(report);
|
|
await tx.done;
|
|
} else {
|
|
lazy.logConsole.debug(`reached cap, nextReset: ${cap.nextReset}`);
|
|
}
|
|
} catch (err) {
|
|
if (err.name === "NotFoundError") {
|
|
console.error(
|
|
`Object store ${FREQ_CAP_STORE} or ${REPORT_STORE} not found`
|
|
);
|
|
} else {
|
|
console.error("IndexedDB access error:", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Deletes any pending report or freq cap data from DB
|
|
*/
|
|
async deleteState() {
|
|
const taskIds = Object.keys(this._tasks);
|
|
const tx = (await this.db).transaction(
|
|
[REPORT_STORE, FREQ_CAP_STORE],
|
|
"readwrite"
|
|
);
|
|
const reportStore = tx.objectStore(REPORT_STORE);
|
|
const capStore = tx.objectStore(FREQ_CAP_STORE);
|
|
|
|
for (const taskId of taskIds) {
|
|
reportStore.delete(taskId);
|
|
}
|
|
|
|
for (const taskId of taskIds) {
|
|
capStore.delete(taskId);
|
|
}
|
|
await tx.done;
|
|
}
|
|
|
|
/* Shutdowns DAPReportCollector, sends any pending reports
|
|
* and deletes any pending report or freq cap data from DB
|
|
*/
|
|
async cleanup(timeout, reason) {
|
|
lazy.clearTimeout(this._timerId);
|
|
await this.submit(timeout, reason);
|
|
await this.deleteState();
|
|
}
|
|
|
|
async startTimedSubmission() {
|
|
await this.submit(30 * 1000, "periodic");
|
|
this._timerId = lazy.setTimeout(
|
|
() => this.startTimedSubmission(),
|
|
this.#timeoutValue()
|
|
);
|
|
}
|
|
#timeoutValue() {
|
|
return this._submissionTimerMins * MINUTE_IN_MILLI;
|
|
}
|
|
|
|
async getReportToSubmit(taskId) {
|
|
const tx = (await this.db).transaction(REPORT_STORE, "readonly");
|
|
const report = await tx.objectStore(REPORT_STORE).get(taskId);
|
|
await tx.done;
|
|
return report;
|
|
}
|
|
|
|
async submit(timeout, reason) {
|
|
let sendPromises = [];
|
|
|
|
for (const [taskId, metadata] of Object.entries(this._tasks)) {
|
|
let task = {
|
|
id: taskId,
|
|
vdaf: metadata._vdaf,
|
|
bits: metadata._bits,
|
|
length: metadata._length,
|
|
time_precision: 60,
|
|
};
|
|
let measurement = metadata._defaultMeasurement;
|
|
let report = await this.getReportToSubmit(taskId);
|
|
if (report) {
|
|
measurement = report.measurement;
|
|
}
|
|
|
|
sendPromises.push(
|
|
DAPTelemetrySender.sendDAPMeasurement(task, measurement, {
|
|
timeout,
|
|
reason,
|
|
})
|
|
);
|
|
|
|
if (report) {
|
|
this.#releasePendingReport(report);
|
|
}
|
|
}
|
|
try {
|
|
await Promise.all(sendPromises);
|
|
} catch (e) {
|
|
lazy.logConsole.error("Failed to send report: ", e);
|
|
}
|
|
}
|
|
}
|