Skip to content

Commit a494af5

Browse files
authored
Tidy up README (#26)
1 parent 39955e6 commit a494af5

File tree

2 files changed

+59
-136
lines changed

2 files changed

+59
-136
lines changed

README.md

Lines changed: 56 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -112,85 +112,20 @@ export function run<T>(id: string, cb: () => T) {
112112
}
113113
```
114114

115-
## Determine the initiator of the task
116-
117-
```typescript
118-
export async function handler(ctx, next) {
119-
const span = Tracer.startSpan();
120-
// First query runs in the synchronous context of the request.
121-
await dbQuery({ criteria: 'item > 10' });
122-
// What about subsequent async operations?
123-
await dbQuery({ criteria: 'item < 10' });
124-
span.finish();
125-
}
126-
127-
async function dbQuery(query) {
128-
// How do we determine which request context we are in?
129-
const span = Tracer.startSpan();
130-
await db.query(query);
131-
span.finish();
132-
}
133-
```
134-
135-
In Node.js applications, we can orchestrate many downstream services to provide
136-
a composite data to users. What the thing is, if the application goes a long
137-
unresponsive downtime, it can be hard to determine which step in our app caused
138-
the issue.
139-
140-
Node.js builtin api `AsyncLocalStorage` can be used to maitain the async task
141-
flow of the request-response and retrieve the initiator of the task. However,
142-
this is not part of the standard and is not available on other runtimes.
143-
144115
## Summary
145116

146-
Tracked async context across executions of async tasks are useful for debugging,
147-
testing, and profiling. With async context tracked, we can propagate values in
148-
the context along the async flow, in which additional datum can be stored and
149-
fetched from without additional manual context transferring, like additional
150-
function parameters. Things can be possible without many change of code to
151-
introduce async re-entrance to current libraries.
152-
153-
While monkey-patching is quite straightforward solution to track async tasks,
154-
there is no way to patch JavaScript features like `async`/`await`. Also,
155-
monkey-patching only works if all third-party libraries with custom scheduling
156-
call a corresponding task awareness registration like
157-
[`AsyncResource.runInAsyncScope`][]. Furthermore, for those custom scheduling
158-
third-party libraries, we need to get library owners to think in terms of async
159-
context propagation.
160-
161-
In a summary, we would like to have an async context tracking specification right
162-
in place of ECMAScript for host environments to take advantage of it, and a
163-
standard JavaScript API to enable third-party libraries to work on different
164-
host environments seamlessly.
165-
166-
Priorities:
167-
1. **Must** be able to automatically link continuous async tasks.
168-
1. **Must** provide a way to enable logical re-entrancy.
169-
1. **Must** not collide or introduce implicit behavior on multiple tracking
170-
instance on single async flow.
117+
This proposal introduces APIs to propagate a value through asynchronous
118+
hop or continuation, such as a promise continuation or async callbacks.
171119

172120
Non-goals:
173-
1. Async task tracking and monitoring. Giving access to task scheduling in
174-
ECMAScript surfaces concerns from secure ECMAScript environments as it
175-
potentially breaking the scopes that a snippet of code can reach.
176-
1. Error handling & bubbling through async stacks. We'd like to discuss this
177-
topic in a separate proposal since this can be another big story to tell, and
178-
keep this proposal minimal and easy to use for most of the case.
179-
1. Async task interception: This can be a cool feature. But it is easy to cause
180-
confusion if some imported library can take application owner unaware actions
181-
to change the application code running pattern. If there are multiple tracking
182-
instance on same async flow, interception can cause collision and implicit
183-
behavior if these instances do not cooperate well. Thus at this very initial
184-
proposal, we'd like to keep the proposal minimal, and discuss this feature in a
185-
follow up proposal.
186-
187-
# Possible Solution
121+
1. Async tasks scheduling and interception.
122+
1. Error handling & bubbling through async stacks.
123+
124+
# Proposed Solution
188125

189126
`AsyncContext` are designed as a value store for context propagation across
190127
multiple logically-connected sync/async operations.
191128

192-
## AsyncContext
193-
194129
```typescript
195130
class AsyncContext<T> {
196131
static wrap<R>(callback: (...args: any[]) => R): (...args: any[]) => R;
@@ -269,32 +204,45 @@ function randomTimeout() {
269204
> Note: There are controversial thought on the dynamic scoping and `AsyncContext`,
270205
> checkout [SCOPING.md][] for more details.
271206
272-
### Using AsyncContext
207+
# Examples
208+
209+
## Determine the initiator of a task
210+
211+
Application monitoring tools like OpenTelemetry save their tracing spans in the
212+
`AsyncContext` and retrieve the span when they need to determine what started
213+
this chain of interaction.
273214

274-
#### Time tracker
215+
These libraries can not intrude the developer APIs for seamless monitoring.
216+
The tracing span doesn't need to be manually passing around by usercodes.
275217

276218
```typescript
277-
// tracker.js
219+
// tracer.js
278220

279221
const context = new AsyncContext();
280222
export function run(cb) {
281223
// (a)
282-
context.run({ startTime: Date.now() }, cb);
224+
const span = {
225+
startTime: Date.now(),
226+
traceId: randomUUID(),
227+
spanId: randomUUID(),
228+
};
229+
context.run(span, cb);
283230
}
284231

285-
export function elapsed() {
232+
export function end() {
286233
// (b)
287-
const elapsed = Date.now() - context.get().startTime;
288-
console.log('onload duration:', elapsed);
234+
const span = context.get();
235+
span?.endTime = Date.now();
289236
}
290237
```
291238

292239
```typescript
293-
import * as tracker from './tracker.js'
240+
// my-app.js
241+
import * as tracer from './tracer.js'
294242

295243
button.onclick = e => {
296244
// (1)
297-
tracker.run(() => {
245+
tracer.run(() => {
298246
fetch("https://example.com").then(res => {
299247
// (2)
300248

@@ -305,79 +253,54 @@ button.onclick = e => {
305253
<button>OK, cool</button></dialog>`;
306254
dialog.show();
307255

308-
tracker.elapsed();
256+
tracer.end();
309257
});
310258
});
311259
});
312260
};
313261
```
314262

315-
In the example above, `run` and `elapsed` don't share same lexical scope with
263+
In the example above, `run` and `end` don't share same lexical scope with
316264
actual code functions, and they are capable of async reentrance thus capable of
317265
concurrent multi-tracking.
318266

319-
#### Request Context Maintenance
267+
## Transitive task attribution
320268

321-
With AsyncContext, maintaining a request context across different execution
322-
context is possible. For example, we'd like to print a log before each database
323-
query with the request trace id.
324-
325-
First we'll have a module holding the async local instance.
269+
User tasks can be scheduled with attributions. With `AsyncContext`, task
270+
attributions are propagated in the async task flow and sub-tasks can be
271+
scheduled with the same priority.
326272

327273
```typescript
328-
// context.js
329-
const context = new AsyncContext();
330-
331-
export function run(ctx, cb) {
332-
context.run(ctx, cb);
333-
}
274+
const scheduler = {
275+
context: new AsyncContext(),
276+
postTask(task, options) {
277+
// In practice, the task execution may be deferred.
278+
// Here we simply run the task immediately with the context.
279+
this.context.run({ priority: options.priority }, task);
280+
},
281+
currentTask() {
282+
return this.context.get() ?? { priority: 'default' };
283+
},
284+
};
334285

335-
export function getContext() {
336-
return context.getValue();
337-
}
338-
```
286+
const res = await scheduler.postTask(task, { priority: 'background' });
287+
console.log(res);
339288

340-
With our owned instance of async context, we can set the value on each request
341-
handling call. After setting the context's value, any operations afterwards can
342-
fetch the value with the instance of async context.
289+
async function task() {
290+
// Fetch remains background priority by referring to scheduler.currentPriority().
291+
const resp = await fetch('/hello');
292+
const text = await resp.text();
343293

344-
```typescript
345-
import { createServer } from 'http';
346-
import { run } from './context.js';
347-
import { queryDatabase } from './db.js';
348-
349-
const server = createServer(handleRequest);
350-
351-
async function handleRequest(req, res) {
352-
run({ req }, () => {
353-
// ... do some async work
354-
// await...
355-
// await...
356-
const result = await queryDatabase({ very: { complex: { query: 'NOT TRUE' } } });
357-
res.statusCode = 200;
358-
res.end(result);
359-
});
294+
scheduler.currentTask(); // => { priority: 'background' }
295+
return doStuffs(text);
360296
}
361-
```
362297

363-
So we don't need an additional parameter to the database query functions.
364-
Still, it's easy to fetch the request data and print it.
365-
366-
```typescript
367-
// db.js
368-
import { getContext } from './context.js';
369-
export function queryDatabase(query) {
370-
const ctx = getContext();
371-
console.log('query database by request %o with query %o',
372-
ctx.req.traceId,
373-
query);
374-
return doQuery(query);
298+
async function doStuffs(text) {
299+
// Some async calculation...
300+
return text;
375301
}
376302
```
377303

378-
In this way, we can have a context value propagated across the async execution
379-
flow and keep track of the value without any other efforts.
380-
381304
# Prior Arts
382305

383306
## zones.js

SCOPING.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ process is that there are potential dynamic scoping of the semantics of
77
### Dynamic Scoping
88

99
A classic dynamic scoping issue is: the variable `x` inside a function `g` will
10-
be determined by the callee of `g`. If `g` is called at root scope, the name `x`
10+
be determined by the caller of `g`. If `g` is called at root scope, the name `x`
1111
refers to the one defined in the root scope. If `g` is called inside a function
1212
`f`, the name `x` could refer to the one defined in the scope of `f`.
1313

@@ -66,10 +66,10 @@ function f() {
6666
}
6767
```
6868
69-
### Dynamic Scoping: dependency on callee
69+
### Dynamic Scoping: dependency on caller
7070
7171
One argument on the dynamic scoping is that the values in `AsyncContext` can be
72-
changed depending on which the callee is.
72+
changed depending on which the caller is.
7373
7474
However, the definition of whether the value of an async context can be changed
7575
has the same meaning with a regular JavaScript variable: anyone with direct

0 commit comments

Comments
 (0)