JavaScript Async Contexts
Updates & for Stage 1
Chengzhong Wu (Alibaba) 2020-07
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.
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.
Motivation
Ergonomically track async contexts in JavaScript
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;
});
}
Security Concerns
Security Concerns
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);
}
Updates
🚧 on possible solution
The hooks system
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.
Regarding to raised concerns
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.
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.
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.
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');
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
Next Steps
Stage 1: Exploring the implementation spaces between the major value propagation strategies
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);
})();
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);
}