Skip to content

Latest commit

 

History

History
114 lines (90 loc) · 3.87 KB

post-task-propagation.md

File metadata and controls

114 lines (90 loc) · 3.87 KB

Propagating and Inheriting postTask Priority

A shared TaskSignal represents a relationship between two tasks, and two tasks that share a signal can be reprioritized together. A frequent request we see from web developers pertaining to scheduling is the desire to inherit or propagate the currently running task's priority. Consider the following example:

function asyncSubtask() {}

function task1() {
  // ... do task1 work ...
  scheduler.postTask(asyncSubtask, { priority: ?? });
  // ... do more work ...
}

scheduler.postTask(task1, { priority: 'background' });

When task1 runs, it might be the case that either or both of the following hold:

  1. asyncSubtask should be have the same priority as task1.
  2. asyncSubtask should be canceled if task1 is aborted.

TaskSignals can solve both (1) and (2), but in the MVP API they need to be explicitly passed to any function that might need them, which can be onerous.

Any function that needs the current signal would need to be modified to take an argument. The example can be rewritten as follows:

function asyncSubtask() {}

function task1(signal) {
  // ... do task1 work ...
  scheduler.postTask(asyncSubtask, { signal });
  // ... do more work ...
}

const controller = new TaskController({priority: 'background'});
scheduler.postTask(task1, { signal }, signal);

Rather than forcing developers to explicitly pass signals everywhere, we are proposing to expose the current TaskSignal so it can be inherited across async boundaries. The current TaskSignal is propagated through postTask and through any Promise chains that begin inside of a postTask task, but will not propagate through other callbacks (e.g., asynchronous event handlers, setTimeout, etc.). When there is no current TaskSignal, scheduler.currentTaskSignal will return a default TaskSignal, representing a default priority with no way to cancel the task. For example:

function asyncSubtask() {}

function task1() {
  // ... do task1 work ...

  // Inherit the current signal.
  let res = scheduler.postTask(asyncSubtask, {signal: scheduler.currentTaskSignal});
  res.then(() => {
    // The currentTaskSignal is retained across this async boundary as well,
    // i.e. when the microtask runs.
    scheduler.postTask(asyncSubtask, {signal: scheduler.currentTaskSignal});
  });
  // ... do more work ...
}

const controller = new TaskController({priority: 'background'});
scheduler.postTask(task1, { signal });

If the priority doesn't need to be changed, i.e. a TaskController wasn't created, a special scheduler.currentTaskSignal can be created so that propagation can still occur:

function asyncSubtask() {}

function task1() {
  // logs 'background'.
  console.log(scheduler.currentTaskSignal.priority);
}

scheduler.postTask(task1, { priority: 'background' });

It is also possible to mix and match priorities and TaskSignals. In this example, if controller.abort() is called before asyncSubtask() runs, it will still be cancelled, even though it is running at a different priority than controller.signal.priority. This is accomplished by creating an implicit TaskSignal with no associated TaskController instead of inheriting controller.signal in its entirety. This implicit TaskSignal follows the controller.signal for abort, but has its own priority that does not change when controller.signal.priority changes.

function asyncSubtask() {
  // logs 'background'.
  console.log(scheduler.currentTaskSignal.priority);
}

function task1() {
  // ... do task1 work ...
  scheduler.postTask(asyncSubtask,
                     {signal: scheduler.currentTaskSignal, priority: 'background'});
  // ... do more work ...
}

const controller = new TaskController({priority: 'user-blocking'});
scheduler.postTask(task1, { controller.signal });