1 of 20

JavaScript Async Contexts

Updates & for Stage 1

Chengzhong Wu (Alibaba) 2020-07

2 of 20

Where did we come to here?

TypeError: Failed to fetch

at rejectPromise

window.onload = e => {

fetch("https://no-exist.com").then(res => {

// doing something...

});

};

Error thrown in host provided async operations, like fetch API and net/http modules in Node.js, didn’t reflect the real cause location.

3 of 20

Where did we come to here? 🤔

export async function handler(ctx, next) {

const span = Tracer.startSpan();

// First query runs in the synchronous context of the request.

await dbQuery({ criteria: 'item > 10' });

// What about subsequent async operations?

await dbQuery({ criteria: 'item < 10' });

span.finish();

}

async function dbQuery(query) {

// How do we determine which request context we are in?

const span = Tracer.startSpan();

await db.query(query);

span.finish();

}

It can be very helpful to have ThreadLocal/AsyncLocal like features in JavaScript.

4 of 20

Motivation

Ergonomically track async contexts in JavaScript

5 of 20

What is “async context”s

document.getElementById('button').onclick = e => {

// (1)

fetch("https://example.com").then(res => {

// (2)

return processBody(res.body).then(data => {

// (5)

const dialog = html`<dialog>Here's some cool data: ${data}

<button>OK, cool</button></dialog>`;

dialog.show();

dialog.querySelector("button").onclick = () => {

// (6)

dialog.close();

};

});

});

};

function processBody(body) {

// (3)

return body.json().then(obj => {

// (4)

return obj.data;

});

}

6 of 20

Security Concerns

7 of 20

Security Concerns

  1. It is possible to get hands on any host defined asynchronous resources and user code scheduling with AsyncHooks.
  2. It is possible to hook into JavaScript code execution between the host actions.
  3. The global hooking may insert arbitrary codes into unawaring user codes.

8 of 20

Previously on the proposal

class AsyncLocalStorage {

constructor();

enterWith(store: any);

exit();

getStore(): any;

}

class AsyncHook {

constructor(hookSpec);

enable();

disable();

}

interface HookSpec {

scheduledAsyncTask(task, triggerTask);

beforeAsyncTaskExecute(task);

afterAsyncTaskExecute(task);

}

  1. No listener provided by AsyncLocalStorage.
  2. Changes may be inferred by AsyncHooks callbacks.
  3. No way to get notified on value changes.

9 of 20

Updates

🚧 on possible solution

10 of 20

The hooks system

  • Host platforms can defined their own hooking systems, may or may not exposing into JavaScript environment.
  • async_hooks will still available in Node.js, but not in the proposal.
  • Secure environments can provide async resource debugging/tracing in developer tools.

11 of 20

No global accessing async local

class AsyncLocal<T = any> {

constructor(valueChangedListener: ValueChangedListener<T>);

getValue(): T;

setValue(value: T);

}

type ValueChangedListener<T> = (newValue: T, prevValue: T) => void;

Most exciting features built on top of async contexts tracking. A “context” store for async flows like user interaction flow, http server request flow.

12 of 20

Regarding to raised concerns

  • No longer exposing host defined async objects and user space code execution scheduling.
  • Value changed listener will only be called on the time that the value of AsyncLocal instance explicitly changes.
  • Value of AsyncLocal can only be changed when those code snippets that directly referencing the AsyncLocal instance.

13 of 20

Example: Timing

// tracker.js

const store = new AsyncLocal();

export function start() {

// (a)

store.setValue({ startTime: Date.now() });

}

export function end() {

// (b)

const dur = Date.now() - store.getValue().startTime;

console.log('onload duration:', dur);

}

import * as tracker from 'tracker.js'

window.onload = e => {

// (1)

tracker.start()

fetch("https://example.com").then(res => {

// (2)

return processBody(res.body).then(data => {

// (3)

const dialog = html`<dialog>Here's some cool data: ${data}

<button>OK, cool</button></dialog>`;

dialog.show();

tracker.end();

});

});

};

Simple re-entrancy implementation of calculating time consumed of a series of async operations without additional context.

14 of 20

Example: Request Context

// context.js

const asyncLocal = new AsyncLocal();

export function setContext(ctx) {

asyncLocal.setValue(ctx);

}

export function getContext() {

return asyncLocal.getValue();

}

import { createServer } from 'http';

import { setContext } from './context.js';

import { queryDatabase } from './db.js';

const server = createServer(handleRequest);

async function handleRequest(req, res) {

setContext({ req });

// ... do some async work

// await...

// await...

const result = await queryDatabase({ very: { complex: { query: 'NOT TRUE' } } });

res.statusCode = 200;

res.end(result);

}

Tracking server incoming request contexts with AsyncLocal.

15 of 20

Example: Request Context

// db.js

import { getContext } from './context.js';

export function queryDatabase(query) {

const ctx = getContext();

console.log('query database by request %o with query %o',

ctx.req.traceId,

query);

return doQuery(query);

}

Instrumenting database accesses with request contexts tracked. Without arduous efforts to get access to current request context.

16 of 20

Value Changed Listeners

const asyncLocal = new AsyncLocal(

(newValue, prevValue) =>

console.log(`valueChanged: newValue(${newValue}), prevValue(${prevValue})`)

);

// Evaluate the `run` function twice asynchronously.

Promise.resolve().then(run);

Promise.resolve().then(run);

async function run() {

// (1) asyncLocal.setValue('foo');

await sleep(1000);

await next(asyncLocal);

// (3) asyncLocal.setValue('quz');

}

async function next() {

// (2) asyncLocal.setValue('bar');

await sleep(1000);

}

// (1)

valueChanged: newValue('foo'), prevValue(undefined);

valueChanged: newValue('foo'), prevValue(undefined);

// (2)

valueChanged: newValue('bar'), prevValue('foo');

valueChanged: newValue('bar'), prevValue('foo');

// (3)

valueChanged: newValue('quz'), prevValue('bar');

valueChanged: newValue('quz'), prevValue('bar');

17 of 20

API for Library Owners

Manual declaring an async logical flow in which a series of async operations will be triggered. Tracking outstanding third-party library with custom scheduling.

class AsyncTask {

constructor();

runInAsyncScope(callback[, thisArg, ...args]);

}

No Updates for AsyncTask

18 of 20

Next Steps

Stage 1: Exploring the implementation spaces between the major value propagation strategies

19 of 20

Possible Value Propagation Solutions

const asyncLocal = new AsyncLocal();

(function main() {

asyncLocal.setValue('main');

setTimeout(() => {

console.log(asyncLocal.getValue()); // => 'main'

asyncLocal.setValue('first timer');

setTimeout(() => {

console.log(asyncLocal.getValue()); // => 'first timer'

}, 1000);

}, 1000);

setTimeout(() => {

console.log(asyncLocal.getValue()); // => 'main'

asyncLocal.setValue('second timer');

setTimeout(() => {

console.log(asyncLocal.getValue()); // => 'second timer'

}, 1000);

}, 1000);

})();

20 of 20

Possible Value Propagation Solutions

const asyncLocal = new AsyncLocal();

(async function main() {

asyncLocal.setValue(0);

await Promise.all([ test(), test() ]);

console.log(asyncLocal.getValue());

})();

async function test() {

console.log(asyncLocal.getValue());

asyncLocal.setValue(asyncLocal.getValue() + 1);

}