mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	This patch provides a task queue helper that supports the following: - Parallel reads + sequential writes. Running tasks as fast as possible. - Queue lifetime tied to extension lifetime, but without requiring an Extension to be around, because that is not always available (e.g. for operations past add-on uninstall). Differential Revision: https://phabricator.services.mozilla.com/D223016
		
			
				
	
	
		
			275 lines
		
	
	
	
		
			9.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			275 lines
		
	
	
	
		
			9.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | 
						|
/* vim: set sts=2 sw=2 et tw=80: */
 | 
						|
"use strict";
 | 
						|
 | 
						|
const { ExtensionTaskScheduler } = ChromeUtils.importESModule(
 | 
						|
  "resource://gre/modules/ExtensionTaskScheduler.sys.mjs"
 | 
						|
);
 | 
						|
 | 
						|
function assertIsPromise(value, message) {
 | 
						|
  let t = typeof value == "object" && value && Cu.getClassName(value, true);
 | 
						|
  equal(t, "Promise", message);
 | 
						|
}
 | 
						|
 | 
						|
add_task(function synchronous_return_value() {
 | 
						|
  const ets = new ExtensionTaskScheduler();
 | 
						|
  const dummyId = "@not_an_extension";
 | 
						|
 | 
						|
  let read1 = ets.runReadTask(dummyId, () => 1);
 | 
						|
  equal(read1, 1, "runReadTask returns return value");
 | 
						|
 | 
						|
  let write1 = ets.runWriteTask(dummyId, () => 1);
 | 
						|
  equal(write1, 1, "runWriteTask returns return value");
 | 
						|
});
 | 
						|
 | 
						|
add_task(async function test_throwing_task() {
 | 
						|
  const ets = new ExtensionTaskScheduler();
 | 
						|
  const dummyId = "@not_an_extension";
 | 
						|
 | 
						|
  function funcThrows() {
 | 
						|
    throw new Error("funcThrows");
 | 
						|
  }
 | 
						|
  let rejectedPromise = Promise.reject(new Error("funcRejects"));
 | 
						|
  function funcRejects() {
 | 
						|
    return rejectedPromise;
 | 
						|
  }
 | 
						|
 | 
						|
  Assert.throws(
 | 
						|
    () => ets.runReadTask(dummyId, funcThrows),
 | 
						|
    /funcThrows/,
 | 
						|
    "sync task throws"
 | 
						|
  );
 | 
						|
 | 
						|
  let rv1 = ets.runWriteTask(dummyId, funcRejects);
 | 
						|
  equal(rv1, rejectedPromise, "Got original rejected promise for write task");
 | 
						|
  let rv2 = ets.runReadTask(dummyId, funcThrows);
 | 
						|
  assertIsPromise(rv2, "Got Promise for read task that is blocked on write");
 | 
						|
  let calls = [];
 | 
						|
  let rv3 = ets.runReadTask(dummyId, () => calls.push(1));
 | 
						|
  assertIsPromise(rv3, "Got Promise for read task that does not reject");
 | 
						|
 | 
						|
  await Assert.rejects(rv1, /funcRejects/, "task resolves to rejection");
 | 
						|
  await Assert.rejects(
 | 
						|
    rv2,
 | 
						|
    /funcThrows/,
 | 
						|
    "task resolves to rejection for synchronously thrown error"
 | 
						|
  );
 | 
						|
  // The two await above run two rounds of microtasks, which should be plenty
 | 
						|
  // to get rv1 to complete, and rv2 + rv3 to run in parallel, and rv3's sync
 | 
						|
  // task to complete.
 | 
						|
  Assert.deepEqual(calls, [1], "Next task still runs after rejection");
 | 
						|
  equal(await rv3, 1, "Got result from non-rejecting task");
 | 
						|
});
 | 
						|
 | 
						|
add_task(async function read_is_parallel__and_write_blocks_read() {
 | 
						|
  const ets = new ExtensionTaskScheduler();
 | 
						|
  const dummyId = "@not_an_extension";
 | 
						|
 | 
						|
  // This returns the internal ExtensionBoundTaskQueue instance. Since there is
 | 
						|
  // no extension, it tests the core behavior of the ReadWriteQueue superclass.
 | 
						|
  let q = ets.forExtensionId(dummyId);
 | 
						|
  equal(q.hasPendingTasks(), false, "Task queue is empty");
 | 
						|
 | 
						|
  let calls = [];
 | 
						|
  let delayedTask1 = Promise.withResolvers();
 | 
						|
  let delayedTask2 = Promise.withResolvers();
 | 
						|
  let delayedTask7 = Promise.withResolvers();
 | 
						|
 | 
						|
  let rv1 = q.runReadTask(() => calls.push(1) && delayedTask1.promise);
 | 
						|
  equal(rv1, delayedTask1.promise, "runReadTask returns original promise (1)");
 | 
						|
  equal(q.hasPendingTasks(), true, "Task queue is not empty");
 | 
						|
 | 
						|
  let rv2 = q.runReadTask(() => calls.push(2) && delayedTask2.promise);
 | 
						|
  equal(rv2, delayedTask2.promise, "runReadTask returns original promise (2)");
 | 
						|
 | 
						|
  equal(
 | 
						|
    q.runReadTask(() => 123),
 | 
						|
    123,
 | 
						|
    "runReadTask returns synchronous result immediately"
 | 
						|
  );
 | 
						|
 | 
						|
  let rv3 = q.runWriteTask(() => calls.push(3));
 | 
						|
  let rv4 = q.runReadTask(() => calls.push(4));
 | 
						|
  let rv5 = q.runWriteTask(() => calls.push(5));
 | 
						|
  let rv6 = q.runWriteTask(() => calls.push(6));
 | 
						|
  let rv7 = q.runReadTask(() => calls.push(7) && delayedTask7.promise);
 | 
						|
 | 
						|
  Assert.deepEqual(
 | 
						|
    calls,
 | 
						|
    [1, 2],
 | 
						|
    "Write task 3 awaits earlier read tasks and blocks later tasks"
 | 
						|
  );
 | 
						|
 | 
						|
  delayedTask1.resolve("res1");
 | 
						|
  delayedTask2.resolve("res2");
 | 
						|
  equal(await rv1, "res1", "Got resolved value for task 1, read");
 | 
						|
  equal(await rv2, "res2", "Got resolved value for task 2, read");
 | 
						|
  Assert.deepEqual(
 | 
						|
    calls,
 | 
						|
    [1, 2, 3, 4, 5, 6, 7],
 | 
						|
    "After unblocking the read task, all other non-async tasks ran immediately"
 | 
						|
  );
 | 
						|
 | 
						|
  equal(
 | 
						|
    q.runReadTask(() => 456),
 | 
						|
    456,
 | 
						|
    "Although a previous read task is still pending, it does not block new read"
 | 
						|
  );
 | 
						|
 | 
						|
  equal(q.hasPendingTasks(), true, "Task queue is not empty");
 | 
						|
  equal(ets._extensionBoundQueues.size, 1, "Queue is alive");
 | 
						|
  equal(
 | 
						|
    ets.forExtensionId(dummyId),
 | 
						|
    q,
 | 
						|
    "Same queue was reused despite the non-existing extension ID"
 | 
						|
  );
 | 
						|
 | 
						|
  delayedTask7.resolve("res7");
 | 
						|
  equal(await rv7, "res7", "Last read task ran");
 | 
						|
  equal(ets._extensionBoundQueues.size, 0, "Queue is gone");
 | 
						|
  equal(q.hasPendingTasks(), false, "Task queue is empty");
 | 
						|
 | 
						|
  let allReturnValues = [rv1, rv2, rv3, rv4, rv5, rv6, rv7];
 | 
						|
  for (let [i, rv] of allReturnValues.entries()) {
 | 
						|
    assertIsPromise(rv, `task ${i} returned a Promise`);
 | 
						|
  }
 | 
						|
  Assert.deepEqual(
 | 
						|
    await Promise.all(allReturnValues),
 | 
						|
    ["res1", "res2", 3, 4, 5, 6, "res7"],
 | 
						|
    "All tasks resolved to the expected value"
 | 
						|
  );
 | 
						|
});
 | 
						|
 | 
						|
add_task(async function write_blocks_write() {
 | 
						|
  const ets = new ExtensionTaskScheduler();
 | 
						|
  const dummyId = "@not_an_extension";
 | 
						|
 | 
						|
  let q = ets.forExtensionId(dummyId);
 | 
						|
 | 
						|
  let calls = [];
 | 
						|
  let delayedTask1 = Promise.withResolvers();
 | 
						|
  let delayedTask2 = Promise.withResolvers();
 | 
						|
 | 
						|
  let rv1 = q.runWriteTask(() => calls.push(1) && delayedTask1.promise);
 | 
						|
  equal(rv1, delayedTask1.promise, "runWriteTask (1) returns original promise");
 | 
						|
 | 
						|
  let rv2 = q.runWriteTask(() => calls.push(2) && delayedTask2.promise);
 | 
						|
  notEqual(rv2, delayedTask2.promise, "runWriteTask (2) returns new Promise");
 | 
						|
  Assert.deepEqual(calls, [1], "Second write blocked on first");
 | 
						|
 | 
						|
  delayedTask1.resolve("res1");
 | 
						|
  delayedTask2.resolve("res2");
 | 
						|
  equal(await rv1, "res1", "Got resolved value for task 1, write");
 | 
						|
  equal(await rv2, "res2", "Got resolved value for task 2, write");
 | 
						|
 | 
						|
  equal(q.hasPendingTasks(), false, "Task queue is empty");
 | 
						|
  equal(ets._extensionBoundQueues.size, 0, "Queue is gone");
 | 
						|
});
 | 
						|
 | 
						|
// Tests that the internal task queues are not persisted in memory when the
 | 
						|
// extensionId is not associated with a live extension.
 | 
						|
add_task(async function schedule_non_existing_extension() {
 | 
						|
  const ets = new ExtensionTaskScheduler();
 | 
						|
  const nonExtensionId = "@does-not-exist";
 | 
						|
  let q1 = ets.forExtensionId(nonExtensionId);
 | 
						|
  let q2 = ets.forExtensionId(nonExtensionId);
 | 
						|
  equal(q1, q2, "Returns same queue even if extension has not loaded yet");
 | 
						|
 | 
						|
  function triggerTask() {
 | 
						|
    const funcReturns1 = () => 1;
 | 
						|
    equal(ets.runReadTask(nonExtensionId, funcReturns1), 1, "Task ran");
 | 
						|
  }
 | 
						|
  equal(ets._extensionBoundQueues.size, 1, "Queue is there");
 | 
						|
  triggerTask();
 | 
						|
  equal(ets._extensionBoundQueues.size, 0, "Queue gone after running task");
 | 
						|
 | 
						|
  let q3 = ets.forExtensionId(nonExtensionId);
 | 
						|
  notEqual(q1, q3, "Queue is different");
 | 
						|
 | 
						|
  equal(ets._extensionBoundQueues.size, 1, "Queue is there");
 | 
						|
  triggerTask();
 | 
						|
  equal(ets._extensionBoundQueues.size, 0, "Queue gone after running task");
 | 
						|
});
 | 
						|
 | 
						|
add_task(async function schedule_existing_extension() {
 | 
						|
  const ets = new ExtensionTaskScheduler();
 | 
						|
  let extension = ExtensionTestUtils.loadExtension({});
 | 
						|
  await extension.startup();
 | 
						|
  let extensionId = extension.id;
 | 
						|
 | 
						|
  function triggerTask() {
 | 
						|
    const funcReturns1 = () => 1;
 | 
						|
    equal(ets.runReadTask(extensionId, funcReturns1), 1, "Task ran");
 | 
						|
  }
 | 
						|
 | 
						|
  let q1 = ets.forExtensionId(extensionId);
 | 
						|
  let q2 = ets.forExtensionId(extensionId);
 | 
						|
  equal(q1, q2, "Returns same queue for extension");
 | 
						|
 | 
						|
  equal(ets._extensionBoundQueues.size, 1, "Queue is there");
 | 
						|
  triggerTask();
 | 
						|
  equal(ets._extensionBoundQueues.size, 1, "Queue still there after task");
 | 
						|
 | 
						|
  let q3 = ets.forExtensionId(extensionId);
 | 
						|
  equal(q1, q3, "Queue is the same");
 | 
						|
 | 
						|
  await extension.unload();
 | 
						|
  equal(ets._extensionBoundQueues.size, 0, "Queue gone after extension unload");
 | 
						|
 | 
						|
  triggerTask();
 | 
						|
  equal(ets._extensionBoundQueues.size, 0, "Queue still gone after task run");
 | 
						|
});
 | 
						|
 | 
						|
add_task(async function load_and_unload_without_schedule() {
 | 
						|
  const ets = new ExtensionTaskScheduler();
 | 
						|
  let extension = ExtensionTestUtils.loadExtension({});
 | 
						|
  await extension.startup();
 | 
						|
  let extensionId = extension.id;
 | 
						|
 | 
						|
  let q = ets.forExtensionId(extensionId);
 | 
						|
  equal(q.hasPendingTasks(), false, "Task queue is empty");
 | 
						|
  equal(ets._extensionBoundQueues.size, 1, "Queue is there");
 | 
						|
 | 
						|
  await extension.unload();
 | 
						|
  equal(ets._extensionBoundQueues.size, 0, "Queue gone after extension unload");
 | 
						|
});
 | 
						|
 | 
						|
add_task(async function queue_outlives_extension_with_pending_tasks() {
 | 
						|
  const extensionId = "@extension_that_reloads";
 | 
						|
  const ets = new ExtensionTaskScheduler();
 | 
						|
  let extension;
 | 
						|
  async function startExtension() {
 | 
						|
    extension = ExtensionTestUtils.loadExtension({
 | 
						|
      manifest: { browser_specific_settings: { gecko: { id: extensionId } } },
 | 
						|
    });
 | 
						|
    await extension.startup();
 | 
						|
  }
 | 
						|
 | 
						|
  let q = ets.forExtensionId(extensionId);
 | 
						|
 | 
						|
  await startExtension();
 | 
						|
  equal(ets._extensionBoundQueues.size, 1, "Queue still there");
 | 
						|
  equal(ets.forExtensionId(extensionId), q, "Queue still same after startup");
 | 
						|
 | 
						|
  let delayedTask1 = Promise.withResolvers();
 | 
						|
  let delayedTask2 = Promise.withResolvers();
 | 
						|
  let rv1 = ets.runWriteTask(extensionId, () => delayedTask1.promise);
 | 
						|
  let rv2 = ets.runReadTask(extensionId, () => delayedTask2.promise);
 | 
						|
  notEqual(rv2, delayedTask2.promise, "read task blocked on write task");
 | 
						|
  equal(ets._extensionBoundQueues.size, 1, "Queue is there after task");
 | 
						|
 | 
						|
  await extension.unload();
 | 
						|
  equal(ets._extensionBoundQueues.size, 1, "Queue is there despite unload");
 | 
						|
 | 
						|
  await startExtension();
 | 
						|
  delayedTask1.resolve("res1");
 | 
						|
  delayedTask2.resolve("res2");
 | 
						|
  equal(await rv1, "res1", "Write task finished");
 | 
						|
  equal(await rv2, "res2", "Read task finished");
 | 
						|
  equal(ets._extensionBoundQueues.size, 1, "Queue is there despite reload");
 | 
						|
 | 
						|
  equal(ets.forExtensionId(extensionId), q, "Queue still same after reload");
 | 
						|
 | 
						|
  await extension.unload();
 | 
						|
  equal(ets._extensionBoundQueues.size, 0, "Queue not persisted after unload");
 | 
						|
});
 |