Skip to content

Commit 7cff1e1

Browse files
Flarnaruyadorno
authored andcommitted
async_hooks: add hook to stop propagation
Add hook to AsyncLocalStorage to allow user to stop propagation. This is needed to avoid leaking a store if e.g. the store indicates that its operations are finished or it reached its time to live. PR-URL: #45386 Reviewed-By: Stephen Belanger <[email protected]> Reviewed-By: Minwoo Jung <[email protected]> Reviewed-By: Chengzhong Wu <[email protected]>
1 parent 4128c27 commit 7cff1e1

File tree

3 files changed

+89
-5
lines changed

3 files changed

+89
-5
lines changed

doc/api/async_context.md

+20-1
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,35 @@ Each instance of `AsyncLocalStorage` maintains an independent storage context.
116116
Multiple instances can safely exist simultaneously without risk of interfering
117117
with each other's data.
118118

119-
### `new AsyncLocalStorage()`
119+
### `new AsyncLocalStorage([options])`
120120

121121
<!-- YAML
122122
added:
123123
- v13.10.0
124124
- v12.17.0
125+
changes:
126+
- version: REPLACEME
127+
pr-url: https://github.com/nodejs/node/pull/45386
128+
description: Add option onPropagate.
125129
-->
126130

131+
> Stability: 1 - `options.onPropagate` is experimental.
132+
133+
* `options` {Object}
134+
* `onPropagate` {Function} Optional callback invoked before a store is
135+
propagated to a new async resource. Returning `true` allows propagation,
136+
returning `false` avoids it. Default is to propagate always.
137+
127138
Creates a new instance of `AsyncLocalStorage`. Store is only provided within a
128139
`run()` call or after an `enterWith()` call.
129140

141+
The `onPropagate` is called during creation of an async resource. Throwing at
142+
this time will print the stack trace and exit. See
143+
[`async_hooks` Error handling][] for details.
144+
145+
Creating an async resource within the `onPropagate` callback will result in
146+
a recursive call to `onPropagate`.
147+
130148
### `asyncLocalStorage.disable()`
131149

132150
<!-- YAML
@@ -816,4 +834,5 @@ const server = createServer((req, res) => {
816834
[`EventEmitter`]: events.md#class-eventemitter
817835
[`Stream`]: stream.md#stream
818836
[`Worker`]: worker_threads.md#class-worker
837+
[`async_hooks` Error handling]: async_hooks.md#error-handling
819838
[`util.promisify()`]: util.md#utilpromisifyoriginal

lib/async_hooks.js

+19-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const {
1818
const {
1919
ERR_ASYNC_CALLBACK,
2020
ERR_ASYNC_TYPE,
21+
ERR_INVALID_ARG_TYPE,
2122
ERR_INVALID_ASYNC_ID
2223
} = require('internal/errors').codes;
2324
const { kEmptyObject } = require('internal/util');
@@ -268,15 +269,27 @@ const storageHook = createHook({
268269
const currentResource = executionAsyncResource();
269270
// Value of currentResource is always a non null object
270271
for (let i = 0; i < storageList.length; ++i) {
271-
storageList[i]._propagate(resource, currentResource);
272+
storageList[i]._propagate(resource, currentResource, type);
272273
}
273274
}
274275
});
275276

276277
class AsyncLocalStorage {
277-
constructor() {
278+
constructor(options = kEmptyObject) {
279+
if (typeof options !== 'object' || options === null) {
280+
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
281+
}
282+
283+
const { onPropagate = null } = options;
284+
if (onPropagate !== null && typeof onPropagate !== 'function') {
285+
throw new ERR_INVALID_ARG_TYPE('options.onPropagate',
286+
'function',
287+
onPropagate);
288+
}
289+
278290
this.kResourceStore = Symbol('kResourceStore');
279291
this.enabled = false;
292+
this._onPropagate = onPropagate;
280293
}
281294

282295
disable() {
@@ -300,10 +313,12 @@ class AsyncLocalStorage {
300313
}
301314

302315
// Propagate the context from a parent resource to a child one
303-
_propagate(resource, triggerResource) {
316+
_propagate(resource, triggerResource, type) {
304317
const store = triggerResource[this.kResourceStore];
305318
if (this.enabled) {
306-
resource[this.kResourceStore] = store;
319+
if (this._onPropagate === null || this._onPropagate(type, store)) {
320+
resource[this.kResourceStore] = store;
321+
}
307322
}
308323
}
309324

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const { AsyncLocalStorage, AsyncResource } = require('async_hooks');
5+
6+
let cnt = 0;
7+
function onPropagate(type, store) {
8+
assert.strictEqual(als.getStore(), store);
9+
cnt++;
10+
if (cnt === 1) {
11+
assert.strictEqual(type, 'r1');
12+
return true;
13+
}
14+
if (cnt === 2) {
15+
assert.strictEqual(type, 'r2');
16+
return false;
17+
}
18+
}
19+
20+
const als = new AsyncLocalStorage({
21+
onPropagate: common.mustCall(onPropagate, 2)
22+
});
23+
24+
const myStore = {};
25+
26+
als.run(myStore, common.mustCall(() => {
27+
const r1 = new AsyncResource('r1');
28+
const r2 = new AsyncResource('r2');
29+
r1.runInAsyncScope(common.mustCall(() => {
30+
assert.strictEqual(als.getStore(), myStore);
31+
}));
32+
r2.runInAsyncScope(common.mustCall(() => {
33+
assert.strictEqual(als.getStore(), undefined);
34+
r1.runInAsyncScope(common.mustCall(() => {
35+
assert.strictEqual(als.getStore(), myStore);
36+
}));
37+
}));
38+
}));
39+
40+
assert.throws(() => new AsyncLocalStorage(15), {
41+
message: 'The "options" argument must be of type object. Received type number (15)',
42+
code: 'ERR_INVALID_ARG_TYPE',
43+
name: 'TypeError'
44+
});
45+
46+
assert.throws(() => new AsyncLocalStorage({ onPropagate: 'bar' }), {
47+
message: 'The "options.onPropagate" property must be of type function. Received type string (\'bar\')',
48+
code: 'ERR_INVALID_ARG_TYPE',
49+
name: 'TypeError'
50+
});

0 commit comments

Comments
 (0)