tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

RemoteSettingsWorker.sys.mjs (7308B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * Interface to a dedicated thread handling for Remote Settings heavy operations.
      7 */
      8 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      9 
     10 import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
     11 
     12 const lazy = {};
     13 
     14 XPCOMUtils.defineLazyPreferenceGetter(
     15  lazy,
     16  "gMaxIdleMilliseconds",
     17  "services.settings.worker_idle_max_milliseconds",
     18  30 * 1000 // Default of 30 seconds.
     19 );
     20 
     21 ChromeUtils.defineESModuleGetters(lazy, {
     22  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
     23  SharedUtils: "resource://services-settings/SharedUtils.sys.mjs",
     24 });
     25 
     26 // Note: we currently only ever construct one instance of Worker.
     27 // If it stops being a singleton, the AsyncShutdown code at the bottom
     28 // of this file, as well as these globals, will need adjusting.
     29 let gShutdown = false;
     30 let gShutdownResolver = null;
     31 
     32 class RemoteSettingsWorkerError extends Error {
     33  constructor(message) {
     34    super(message);
     35    this.name = "RemoteSettingsWorkerError";
     36  }
     37 }
     38 
     39 class Worker {
     40  constructor(source) {
     41    if (gShutdown) {
     42      console.error("Can't create worker once shutdown has started");
     43    }
     44    this.source = source;
     45    this.worker = null;
     46 
     47    this.callbacks = new Map();
     48    this.lastCallbackId = 0;
     49    this.idleTimeoutId = null;
     50  }
     51 
     52  async _execute(method, args = [], options = {}) {
     53    // Check if we're shutting down.
     54    if (gShutdown && method != "prepareShutdown") {
     55      throw new RemoteSettingsWorkerError("Remote Settings has shut down.");
     56    }
     57    // Don't instantiate the worker to shut it down.
     58    if (method == "prepareShutdown" && !this.worker) {
     59      return null;
     60    }
     61 
     62    const { mustComplete = false } = options;
     63    // (Re)instantiate the worker if it was terminated.
     64    if (!this.worker) {
     65      this.worker = new ChromeWorker(this.source, { type: "module" });
     66      this.worker.onmessage = this._onWorkerMessage.bind(this);
     67      this.worker.onerror = error => {
     68        // Worker crashed. Reject each pending callback.
     69        for (const { reject } of this.callbacks.values()) {
     70          reject(error);
     71        }
     72        this.callbacks.clear();
     73        // And terminate it.
     74        this.stop();
     75      };
     76    }
     77    // New activity: reset the idle timer.
     78    if (this.idleTimeoutId) {
     79      clearTimeout(this.idleTimeoutId);
     80    }
     81    let identifier = method + "-";
     82    // Include the collection details in the importJSONDump case.
     83    if (identifier == "importJSONDump-") {
     84      identifier += `${args[0]}-${args[1]}-`;
     85    }
     86    return new Promise((resolve, reject) => {
     87      const callbackId = `${identifier}${++this.lastCallbackId}`;
     88      this.callbacks.set(callbackId, { resolve, reject, mustComplete });
     89      this.worker.postMessage({ callbackId, method, args });
     90    });
     91  }
     92 
     93  _onWorkerMessage(event) {
     94    const { callbackId, result, error } = event.data;
     95    // If we're shutting down, we may have already rejected this operation
     96    // and removed its callback from our map:
     97    if (!this.callbacks.has(callbackId)) {
     98      return;
     99    }
    100    const { resolve, reject } = this.callbacks.get(callbackId);
    101    if (error) {
    102      reject(new RemoteSettingsWorkerError(error));
    103    } else {
    104      resolve(result);
    105    }
    106    this.callbacks.delete(callbackId);
    107 
    108    // Terminate the worker when it's unused for some time.
    109    // But don't terminate it if an operation is pending.
    110    if (!this.callbacks.size) {
    111      if (gShutdown) {
    112        this.stop();
    113        if (gShutdownResolver) {
    114          gShutdownResolver();
    115        }
    116      } else {
    117        this.idleTimeoutId = setTimeout(() => {
    118          this.stop();
    119        }, lazy.gMaxIdleMilliseconds);
    120      }
    121    }
    122  }
    123 
    124  /**
    125   * Called at shutdown to abort anything the worker is doing that isn't
    126   * critical.
    127   */
    128  _abortCancelableRequests() {
    129    // End all tasks that we can.
    130    const callbackCopy = Array.from(this.callbacks.entries());
    131    const error = new Error("Shutdown, aborting read-only worker requests.");
    132    for (const [id, { reject, mustComplete }] of callbackCopy) {
    133      if (!mustComplete) {
    134        this.callbacks.delete(id);
    135        reject(error);
    136      }
    137    }
    138    // There might be nothing left now:
    139    if (!this.callbacks.size) {
    140      this.stop();
    141      if (gShutdownResolver) {
    142        gShutdownResolver();
    143      }
    144    }
    145    // If there was something left, we'll stop as soon as we get messages from
    146    // those tasks, too.
    147    // Let's hurry them along a bit:
    148    this._execute("prepareShutdown");
    149  }
    150 
    151  stop() {
    152    this.worker.terminate();
    153    this.worker = null;
    154    this.idleTimeoutId = null;
    155  }
    156 
    157  async canonicalStringify(localRecords, remoteRecords, timestamp) {
    158    return this._execute("canonicalStringify", [
    159      localRecords,
    160      remoteRecords,
    161      timestamp,
    162    ]);
    163  }
    164 
    165  async importJSONDump(bucket, collection) {
    166    return this._execute("importJSONDump", [bucket, collection], {
    167      mustComplete: true,
    168    });
    169  }
    170 
    171  async checkFileHash(filepath, size, hash) {
    172    return this._execute("checkFileHash", [filepath, size, hash]);
    173  }
    174 
    175  async checkContentHash(buffer, size, hash) {
    176    // The implementation does little work on the current thread, so run the
    177    // task on the current thread instead of the worker thread.
    178    return lazy.SharedUtils.checkContentHash(buffer, size, hash);
    179  }
    180 }
    181 
    182 // Now, first add a shutdown blocker. If that fails, we must have
    183 // shut down already.
    184 // We're doing this here rather than in the Worker constructor because in
    185 // principle having just 1 shutdown blocker for the entire file should be
    186 // fine. If we ever start creating more than one Worker instance, this
    187 // code will need adjusting to deal with that.
    188 try {
    189  lazy.AsyncShutdown.profileBeforeChange.addBlocker(
    190    "Remote Settings profile-before-change",
    191    async () => {
    192      // First, indicate we've shut down.
    193      gShutdown = true;
    194      // Then, if we have no worker or no callbacks, we're done.
    195      if (
    196        !RemoteSettingsWorker.worker ||
    197        !RemoteSettingsWorker.callbacks.size
    198      ) {
    199        return null;
    200      }
    201      // Otherwise, there's something left to do. Set up a promise:
    202      let finishedPromise = new Promise(resolve => {
    203        gShutdownResolver = resolve;
    204      });
    205 
    206      // Try to cancel most of the work:
    207      RemoteSettingsWorker._abortCancelableRequests();
    208 
    209      // Return a promise that the worker will resolve.
    210      return finishedPromise;
    211    },
    212    {
    213      fetchState() {
    214        const remainingCallbacks = RemoteSettingsWorker.callbacks;
    215        const details = Array.from(remainingCallbacks.keys()).join(", ");
    216        return `Remaining: ${remainingCallbacks.size} callbacks (${details}).`;
    217      },
    218    }
    219  );
    220 } catch (ex) {
    221  console.error(
    222    "Couldn't add shutdown blocker, assuming shutdown has started."
    223  );
    224  console.error(ex);
    225  // If AsyncShutdown throws, `profileBeforeChange` has already fired. Ignore it
    226  // and mark shutdown. Constructing the worker will report an error and do
    227  // nothing.
    228  gShutdown = true;
    229 }
    230 
    231 export var RemoteSettingsWorker = new Worker(
    232  "resource://services-settings/RemoteSettings.worker.mjs"
    233 );