Skip to content

Commit

Permalink
async_hooks: add store arg in AsyncLocalStorage
Browse files Browse the repository at this point in the history
This commit introduces store as the first argument in
AsyncLocalStorage's run methods. The change is motivated by the
following expectation: most users are going to use a custom object
as the store and an extra Map created by the previous implementation
is an overhead for their use case.

Important note. This is a backwards incompatible change.
It was discussed and agreed an incompatible change is ok
since the API is still experimental and the modified
methods were only added within the last week so usage
will be minimal to none.

PR-URL: #31930
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
  • Loading branch information
puzpuzpuz authored and targos committed Apr 28, 2020
1 parent 16e8b11 commit 96d1f14
Show file tree
Hide file tree
Showing 14 changed files with 73 additions and 47 deletions.
6 changes: 3 additions & 3 deletions benchmark/async_hooks/async-resource-vs-destroy.js
Expand Up @@ -106,7 +106,7 @@ function buildDestroy(getServe) {
function buildAsyncLocalStorage(getServe) {
const asyncLocalStorage = new AsyncLocalStorage();
const server = createServer((req, res) => {
asyncLocalStorage.runSyncAndReturn(() => {
asyncLocalStorage.runSyncAndReturn({}, () => {
getServe(getCLS, setCLS)(req, res);
});
});
Expand All @@ -118,12 +118,12 @@ function buildAsyncLocalStorage(getServe) {

function getCLS() {
const store = asyncLocalStorage.getStore();
return store.get('store');
return store.state;
}

function setCLS(state) {
const store = asyncLocalStorage.getStore();
store.set('store', state);
store.state = state;
}

function close() {
Expand Down
46 changes: 25 additions & 21 deletions doc/api/async_hooks.md
Expand Up @@ -900,7 +900,7 @@ function log(...args) {
}

http.createServer((request, response) => {
asyncLocalStorage.run(() => {
asyncLocalStorage.run(new Map(), () => {
const store = asyncLocalStorage.getStore();
store.set(kReq, request);
someAsyncOperation((err, result) => {
Expand Down Expand Up @@ -950,27 +950,27 @@ in the current process.
added: REPLACEME
-->

* Returns: {Map}
* Returns: {any}

This method returns the current store.
If this method is called outside of an asynchronous context initialized by
calling `asyncLocalStorage.run` or `asyncLocalStorage.runAndReturn`, it will
return `undefined`.

### `asyncLocalStorage.run(callback[, ...args])`
### `asyncLocalStorage.run(store, callback[, ...args])`
<!-- YAML
added: REPLACEME
-->

* `store` {any}
* `callback` {Function}
* `...args` {any}

Calling `asyncLocalStorage.run(callback)` will create a new asynchronous
context.
Within the callback function and the asynchronous operations from the callback,
`asyncLocalStorage.getStore()` will return an instance of `Map` known as
"the store". This store will be persistent through the following
asynchronous calls.
context. Within the callback function and the asynchronous operations from
the callback, `asyncLocalStorage.getStore()` will return the object or
the primitive value passed into the `store` argument (known as "the store").
This store will be persistent through the following asynchronous calls.

The callback will be ran asynchronously. Optionally, arguments can be passed
to the function. They will be passed to the callback function.
Expand All @@ -982,10 +982,11 @@ Also, the stacktrace will be impacted by the asynchronous call.
Example:

```js
asyncLocalStorage.run(() => {
asyncLocalStorage.getStore(); // Returns a Map
const store = { id: 1 };
asyncLocalStorage.run(store, () => {
asyncLocalStorage.getStore(); // Returns the store object
someAsyncOperation(() => {
asyncLocalStorage.getStore(); // Returns the same Map
asyncLocalStorage.getStore(); // Returns the same object
});
});
asyncLocalStorage.getStore(); // Returns undefined
Expand Down Expand Up @@ -1014,20 +1015,21 @@ Also, the stacktrace will be impacted by the asynchronous call.
Example:

```js
asyncLocalStorage.run(() => {
asyncLocalStorage.getStore(); // Returns a Map
asyncLocalStorage.run('store value', () => {
asyncLocalStorage.getStore(); // Returns 'store value'
asyncLocalStorage.exit(() => {
asyncLocalStorage.getStore(); // Returns undefined
});
asyncLocalStorage.getStore(); // Returns the same Map
asyncLocalStorage.getStore(); // Returns 'store value'
});
```

### `asyncLocalStorage.runSyncAndReturn(callback[, ...args])`
### `asyncLocalStorage.runSyncAndReturn(store, callback[, ...args])`
<!-- YAML
added: REPLACEME
-->

* `store` {any}
* `callback` {Function}
* `...args` {any}

Expand All @@ -1045,9 +1047,10 @@ the context will be exited.
Example:

```js
const store = { id: 2 };
try {
asyncLocalStorage.runSyncAndReturn(() => {
asyncLocalStorage.getStore(); // Returns a Map
asyncLocalStorage.runSyncAndReturn(store, () => {
asyncLocalStorage.getStore(); // Returns the store object
throw new Error();
});
} catch (e) {
Expand Down Expand Up @@ -1080,13 +1083,13 @@ Example:
```js
// Within a call to run or runSyncAndReturn
try {
asyncLocalStorage.getStore(); // Returns a Map
asyncLocalStorage.getStore(); // Returns the store object or value
asyncLocalStorage.exitSyncAndReturn(() => {
asyncLocalStorage.getStore(); // Returns undefined
throw new Error();
});
} catch (e) {
asyncLocalStorage.getStore(); // Returns the same Map
asyncLocalStorage.getStore(); // Returns the same object or value
// The error will be caught here
}
```
Expand All @@ -1112,8 +1115,9 @@ It cannot be promisified using `util.promisify`. If needed, the `Promise`
constructor can be used:

```js
const store = new Map(); // initialize the store
new Promise((resolve, reject) => {
asyncLocalStorage.run(() => {
asyncLocalStorage.run(store, () => {
someFunction((err, result) => {
if (err) {
return reject(err);
Expand Down Expand Up @@ -1142,7 +1146,7 @@ the following pattern should be used:

```js
async function fn() {
await asyncLocalStorage.runSyncAndReturn(() => {
await asyncLocalStorage.runSyncAndReturn(new Map(), () => {
asyncLocalStorage.getStore().set('key', value);
return foo(); // The return value of foo will be awaited
});
Expand Down
14 changes: 6 additions & 8 deletions lib/async_hooks.js
@@ -1,11 +1,9 @@
'use strict';

const {
Map,
NumberIsSafeInteger,
ReflectApply,
Symbol,

} = primordials;

const {
Expand Down Expand Up @@ -248,14 +246,14 @@ class AsyncLocalStorage {
}
}

_enter() {
_enter(store) {
if (!this.enabled) {
this.enabled = true;
storageList.push(this);
storageHook.enable();
}
const resource = executionAsyncResource();
resource[this.kResourceStore] = new Map();
resource[this.kResourceStore] = store;
}

_exit() {
Expand All @@ -265,8 +263,8 @@ class AsyncLocalStorage {
}
}

runSyncAndReturn(callback, ...args) {
this._enter();
runSyncAndReturn(store, callback, ...args) {
this._enter(store);
try {
return callback(...args);
} finally {
Expand All @@ -290,8 +288,8 @@ class AsyncLocalStorage {
}
}

run(callback, ...args) {
this._enter();
run(store, callback, ...args) {
this._enter(store);
process.nextTick(callback, ...args);
this._exit();
}
Expand Down
4 changes: 2 additions & 2 deletions test/async-hooks/test-async-local-storage-args.js
Expand Up @@ -5,14 +5,14 @@ const { AsyncLocalStorage } = require('async_hooks');

const asyncLocalStorage = new AsyncLocalStorage();

asyncLocalStorage.run((runArg) => {
asyncLocalStorage.run({}, (runArg) => {
assert.strictEqual(runArg, 1);
asyncLocalStorage.exit((exitArg) => {
assert.strictEqual(exitArg, 2);
}, 2);
}, 1);

asyncLocalStorage.runSyncAndReturn((runArg) => {
asyncLocalStorage.runSyncAndReturn({}, (runArg) => {
assert.strictEqual(runArg, 'foo');
asyncLocalStorage.exitSyncAndReturn((exitArg) => {
assert.strictEqual(exitArg, 'bar');
Expand Down
2 changes: 1 addition & 1 deletion test/async-hooks/test-async-local-storage-async-await.js
Expand Up @@ -12,7 +12,7 @@ async function test() {
}

async function main() {
await asyncLocalStorage.runSyncAndReturn(test);
await asyncLocalStorage.runSyncAndReturn(new Map(), test);
assert.strictEqual(asyncLocalStorage.getStore(), undefined);
}

Expand Down
Expand Up @@ -19,7 +19,7 @@ async function testAwait() {
await asyncLocalStorage.exitSyncAndReturn(testOut);
}

asyncLocalStorage.run(() => {
asyncLocalStorage.run(new Map(), () => {
const store = asyncLocalStorage.getStore();
store.set('key', 'value');
testAwait(); // should not reject
Expand Down
4 changes: 2 additions & 2 deletions test/async-hooks/test-async-local-storage-enable-disable.js
Expand Up @@ -5,15 +5,15 @@ const { AsyncLocalStorage } = require('async_hooks');

const asyncLocalStorage = new AsyncLocalStorage();

asyncLocalStorage.runSyncAndReturn(() => {
asyncLocalStorage.runSyncAndReturn(new Map(), () => {
asyncLocalStorage.getStore().set('foo', 'bar');
process.nextTick(() => {
assert.strictEqual(asyncLocalStorage.getStore().get('foo'), 'bar');
asyncLocalStorage.disable();
assert.strictEqual(asyncLocalStorage.getStore(), undefined);
process.nextTick(() => {
assert.strictEqual(asyncLocalStorage.getStore(), undefined);
asyncLocalStorage.runSyncAndReturn(() => {
asyncLocalStorage.runSyncAndReturn(new Map(), () => {
assert.notStrictEqual(asyncLocalStorage.getStore(), undefined);
});
});
Expand Down
2 changes: 1 addition & 1 deletion test/async-hooks/test-async-local-storage-errors-async.js
Expand Up @@ -13,7 +13,7 @@ process.setUncaughtExceptionCaptureCallback((err) => {
assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'node');
});

asyncLocalStorage.run(() => {
asyncLocalStorage.run(new Map(), () => {
const store = asyncLocalStorage.getStore();
store.set('hello', 'node');
setTimeout(() => {
Expand Down
Expand Up @@ -14,7 +14,7 @@ process.setUncaughtExceptionCaptureCallback((err) => {
});

try {
asyncLocalStorage.runSyncAndReturn(() => {
asyncLocalStorage.runSyncAndReturn(new Map(), () => {
const store = asyncLocalStorage.getStore();
store.set('hello', 'node');
setTimeout(() => {
Expand Down
2 changes: 1 addition & 1 deletion test/async-hooks/test-async-local-storage-http.js
Expand Up @@ -10,7 +10,7 @@ const server = http.createServer((req, res) => {
});

server.listen(0, () => {
asyncLocalStorage.run(() => {
asyncLocalStorage.run(new Map(), () => {
const store = asyncLocalStorage.getStore();
store.set('hello', 'world');
http.get({ host: 'localhost', port: server.address().port }, () => {
Expand Down
24 changes: 24 additions & 0 deletions test/async-hooks/test-async-local-storage-misc-stores.js
@@ -0,0 +1,24 @@
'use strict';
require('../common');
const assert = require('assert');
const { AsyncLocalStorage } = require('async_hooks');

const asyncLocalStorage = new AsyncLocalStorage();

asyncLocalStorage.run(42, () => {
assert.strictEqual(asyncLocalStorage.getStore(), 42);
});

const runStore = { foo: 'bar' };
asyncLocalStorage.run(runStore, () => {
assert.strictEqual(asyncLocalStorage.getStore(), runStore);
});

asyncLocalStorage.runSyncAndReturn('hello node', () => {
assert.strictEqual(asyncLocalStorage.getStore(), 'hello node');
});

const runSyncStore = { hello: 'node' };
asyncLocalStorage.runSyncAndReturn(runSyncStore, () => {
assert.strictEqual(asyncLocalStorage.getStore(), runSyncStore);
});
4 changes: 2 additions & 2 deletions test/async-hooks/test-async-local-storage-nested.js
Expand Up @@ -6,9 +6,9 @@ const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();

setTimeout(() => {
asyncLocalStorage.run(() => {
asyncLocalStorage.run(new Map(), () => {
const asyncLocalStorage2 = new AsyncLocalStorage();
asyncLocalStorage2.run(() => {
asyncLocalStorage2.run(new Map(), () => {
const store = asyncLocalStorage.getStore();
const store2 = asyncLocalStorage2.getStore();
store.set('hello', 'world');
Expand Down
6 changes: 3 additions & 3 deletions test/async-hooks/test-async-local-storage-no-mix-contexts.js
Expand Up @@ -7,8 +7,8 @@ const asyncLocalStorage = new AsyncLocalStorage();
const asyncLocalStorage2 = new AsyncLocalStorage();

setTimeout(() => {
asyncLocalStorage.run(() => {
asyncLocalStorage2.run(() => {
asyncLocalStorage.run(new Map(), () => {
asyncLocalStorage2.run(new Map(), () => {
const store = asyncLocalStorage.getStore();
const store2 = asyncLocalStorage2.getStore();
store.set('hello', 'world');
Expand All @@ -28,7 +28,7 @@ setTimeout(() => {
}, 100);

setTimeout(() => {
asyncLocalStorage.run(() => {
asyncLocalStorage.run(new Map(), () => {
const store = asyncLocalStorage.getStore();
store.set('hello', 'earth');
setTimeout(() => {
Expand Down
2 changes: 1 addition & 1 deletion test/async-hooks/test-async-local-storage-promises.js
Expand Up @@ -12,7 +12,7 @@ async function main() {
throw err;
});
await new Promise((resolve, reject) => {
asyncLocalStorage.run(() => {
asyncLocalStorage.run(new Map(), () => {
const store = asyncLocalStorage.getStore();
store.set('a', 1);
next().then(resolve, reject);
Expand Down

0 comments on commit 96d1f14

Please sign in to comment.