AsyncShutdown

在进程关闭期间,子系统会依次关闭。AsyncShutdown 是一个专门用于表达服务及其客户端之间关闭时间依赖关系的模块;

  • 服务和它们的客户端;

  • 关闭阶段(例如 profile-before-change)及其客户端。

屏障:表达对服务的关闭依赖关系

考虑一个服务 FooService。在进程关闭过程中的某个时刻,此服务需要

  • 通知其客户端即将关闭;

  • 等待客户端完成基于 FooService 的最终操作(通常是异步的);

  • 然后才能自行关闭。

这可以用 AsyncShutdown.Barrier 的实例来表达。AsyncShutdown.Barrier 的实例提供

  • 一个功能 client,可以发布给客户端,以便它们注册或注销阻塞器;

  • 屏障所有者可以使用的方法来查询阻塞器状态并等待所有客户端注册的阻塞器都已解决。

关闭超时

根据设计,如果 AsyncShutdown.Barrier 的客户端花费超过 60 秒才能解除或移除其阻塞器,则会导致崩溃(“唤醒”表示不计算计算机处于睡眠状态或过于繁忙而无法执行任何操作的秒数)。此机制有助于确保我们不会使进程处于既无法继续关闭也无法重新启动的状态。

如果启用了 CrashReporter,则此崩溃将报告

示例 1:简单的 Barrier 客户端

以下代码片段展示了一个 FooService 客户端的示例,该客户端对 FooService 具有关闭依赖关系。在这种情况下,客户端希望确保 FooService 在达到某个状态之前不会关闭。例如,客户端需要异步写入数据,并且需要确保在关闭之前已将其状态完全写入磁盘,即使由于某些用户操作导致关闭立即发生。

警告

如果在关闭过程中添加新阻塞器的时间太晚,则 addBlocker() 会抛出异常。使用者必须处理这种情况,尤其是在阻塞器包含解除其他关闭阶段阻塞的代码时。nsIShutdownClient.isClosed 也可用于检查此条件。

// Some client of FooService called FooClient

const { FooService } = ChromeUtils.importESModule(
  "resource://gre/modules/FooService.sys.mjs"
);

// FooService.shutdown is the `client` capability of a `Barrier`.
// See example 2 for the definition of `FooService.shutdown`
FooService.shutdown.addBlocker(
  "FooClient: Need to make sure that we have reached some state",
  () => promiseReachedSomeState
);
// promiseReachedSomeState should be an instance of Promise resolved once
// we have reached the expected state

示例 2:简单的 Barrier 所有者

以下代码片段展示了一个服务 FooService 的示例,该服务希望确保所有客户端都有机会完成任何未完成的操作,然后 FooService 才能关闭。

// Module FooService

const { AsyncShutdown } = ChromeUtils.importESModule(
  "resource://gre/modules/AsyncShutdown.sys.mjs"
);

this.exports = ["FooService"];

let shutdown = new AsyncShutdown.Barrier("FooService: Waiting for clients before shutting down");

// Export the `client` capability, to let clients register shutdown blockers
FooService.shutdown = shutdown.client;

// This function should be triggered at some point during shutdown, generally
// as a client to another Barrier or Phase. Triggering this function is not covered
// in this snippet.
let onshutdown = async function() {
  // Wait for all registered clients to have lifted the barrier
  await shutdown.wait();

  // Now deactivate FooService itself.
  // ...
});

通常,拥有 AsyncShutdown.Barrier 的服务本身也是另一个 Barrier 的客户端。

示例 3:更复杂的 Barrier 客户端

以下代码片段展示了 FooClient2,它是 FooService 的一个更复杂的客户端,它需要在关闭期间但在 FooService 关闭之前执行许多操作。此外,鉴于此客户端更加复杂,我们提供了一个函数来返回 FooClient2 在关闭期间的状态。如果由于某种原因 FooClient2 的阻塞器从未被解除,则可以将此状态作为崩溃报告的一部分进行报告。

// Some client of FooService called FooClient2

const { FooService } = ChromeUtils.importESModule(
  "resource://gre/modules/FooService.sys.mjs"
);

FooService.shutdown.addBlocker(
  "FooClient2: Collecting data, writing it to disk and shutting down",
  () => Blocker.wait(),
  () => Blocker.state
);

let Blocker = {
  // This field contains information on the status of the blocker.
  // It can be any JSON serializable object.
  state: "Not started",

  async wait() {
    // This method is called once FooService starts informing its clients that
    // FooService wishes to shut down.

    // Update the state as we go. If the Barrier is used in conjunction with
    // a Phase, this state will be reported as part of a crash report if FooClient fails
    // to shutdown properly.
    this.state = "Starting";

    let data = await collectSomeData();
    this.state = "Data collection complete";

    try {
      await writeSomeDataToDisk(data);
      this.state = "Data successfully written to disk";
    } catch (ex) {
      this.state = "Writing data to disk failed, proceeding with shutdown: " + ex;
    }

    await FooService.oneLastCall();
    this.state = "Ready";
  }
};

示例 4:具有内部和外部依赖关系的服务

// Module FooService2

let { AsyncShutdown } = ChromeUtils.importESModule(
  "resource://gre/modules/AsyncShutdown.sys.mjs"
);

this.exports = ["FooService2"];

let shutdown = new AsyncShutdown.Barrier("FooService2: Waiting for clients before shutting down");

// Export the `client` capability, to let clients register shutdown blockers
FooService2.shutdown = shutdown.client;

// A second barrier, used to avoid shutting down while any connections are open.
let connections = new AsyncShutdown.Barrier("FooService2: Waiting for all FooConnections to be closed before shutting down");

let isClosed = false;

FooService2.openFooConnection = function(name) {
  if (isClosed) {
    throw new Error("FooService2 is closed");
  }

  let deferred = Promise.withResolvers();
  connections.client.addBlocker("FooService2: Waiting for connection " + name + " to close",  deferred.promise);

  // ...


  return {
    // ...
    // Some FooConnection object. Presumably, it will have additional methods.
    // ...
    close: function() {
      // ...
      // Perform any operation necessary for closing
      // ...

      // Don't hoard blockers.
      connections.client.removeBlocker(deferred.promise);

      // The barrier MUST be lifted, even if removeBlocker has been called.
      deferred.resolve();
    }
  };
};


// This function should be triggered at some point during shutdown, generally
// as a client to another Barrier. Triggering this function is not covered
// in this snippet.
let onshutdown = async function() {
  // Wait for all registered clients to have lifted the barrier.
  // These clients may open instances of FooConnection if they need to.
  await shutdown.wait();

  // Now stop accepting any other connection request.
  isClosed = true;

  // Wait for all instances of FooConnection to be closed.
  await connections.wait();

  // Now finish shutting down FooService2
  // ...
});

阶段:表达对关闭阶段的依赖关系

进程的关闭按阶段进行,例如

  • profileBeforeChange(此阶段完成后,无法保证进程可以访问配置文件目录);

  • webWorkersShutdown(此阶段完成后,JavaScript 将无法再访问工作线程);

与服务一样,阶段也有客户端。例如,所有使用 Web 工作线程的用户都必须在 webWorkersShutdown 阶段结束之前完成对 Web 工作线程的使用。

模块 AsyncShutdown 为一组众所周知的阶段提供了预定义的屏障。提供的每个屏障都会阻止相应的关闭阶段,直到所有客户端都解除其阻塞器。

阶段列表

AsyncShutdown.profileChangeTeardown

希望在观察者通知“profile-change-teardown”期间异步阻塞的客户端的功能。

AsyncShutdown.profileBeforeChange

希望在观察者通知“profile-change-teardown”期间异步阻塞的客户端的功能。屏障解决后,除 Telemetry 之外的客户端不得访问配置文件目录中的文件,并且客户端不得再使用 Telemetry。

AsyncShutdown.sendTelemetry

希望在观察者通知“profile-before-change-telemetry”期间异步阻塞的客户端的功能。屏障解决后,Telemetry 必须停止其操作。

AsyncShutdown.webWorkersShutdown

希望在观察者通知“web-workers-shutdown”期间异步阻塞的客户端的功能。阶段完成后,客户端不得使用 Web 工作线程。