@@ -112,85 +112,20 @@ export function run<T>(id: string, cb: () => T) {
112
112
}
113
113
```
114
114
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
-
144
115
## Summary
145
116
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.
171
119
172
120
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
188
125
189
126
` AsyncContext ` are designed as a value store for context propagation across
190
127
multiple logically-connected sync/async operations.
191
128
192
- ## AsyncContext
193
-
194
129
``` typescript
195
130
class AsyncContext <T > {
196
131
static wrap<R >(callback : (... args : any []) => R ): (... args : any []) => R ;
@@ -269,32 +204,45 @@ function randomTimeout() {
269
204
> Note: There are controversial thought on the dynamic scoping and ` AsyncContext ` ,
270
205
> checkout [ SCOPING.md] [ ] for more details.
271
206
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.
273
214
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.
275
217
276
218
``` typescript
277
- // tracker .js
219
+ // tracer .js
278
220
279
221
const context = new AsyncContext ();
280
222
export function run(cb ) {
281
223
// (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 );
283
230
}
284
231
285
- export function elapsed () {
232
+ export function end () {
286
233
// (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 ( );
289
236
}
290
237
```
291
238
292
239
``` typescript
293
- import * as tracker from ' ./tracker.js'
240
+ // my-app.js
241
+ import * as tracer from ' ./tracer.js'
294
242
295
243
button .onclick = e => {
296
244
// (1)
297
- tracker .run (() => {
245
+ tracer .run (() => {
298
246
fetch (" https://example.com" ).then (res => {
299
247
// (2)
300
248
@@ -305,79 +253,54 @@ button.onclick = e => {
305
253
<button>OK, cool</button></dialog> ` ;
306
254
dialog .show ();
307
255
308
- tracker . elapsed ();
256
+ tracer . end ();
309
257
});
310
258
});
311
259
});
312
260
};
313
261
```
314
262
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
316
264
actual code functions, and they are capable of async reentrance thus capable of
317
265
concurrent multi-tracking.
318
266
319
- #### Request Context Maintenance
267
+ ## Transitive task attribution
320
268
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.
326
272
327
273
``` 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
+ };
334
285
335
- export function getContext() {
336
- return context .getValue ();
337
- }
338
- ```
286
+ const res = await scheduler .postTask (task , { priority: ' background' });
287
+ console .log (res );
339
288
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 ();
343
293
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 );
360
296
}
361
- ```
362
297
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 ;
375
301
}
376
302
```
377
303
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
-
381
304
# Prior Arts
382
305
383
306
## zones.js
0 commit comments