Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

async-hooks: introduce async-storage API #26540

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 34 additions & 3 deletions benchmark/async_hooks/async-resource-vs-destroy.js
Expand Up @@ -8,7 +8,8 @@ const common = require('../common.js');
const {
createHook,
executionAsyncResource,
executionAsyncId
executionAsyncId,
AsyncLocalStorage
} = require('async_hooks');
const { createServer } = require('http');

Expand All @@ -18,7 +19,7 @@ const connections = 500;
const path = '/';

const bench = common.createBenchmark(main, {
type: ['async-resource', 'destroy'],
type: ['async-resource', 'destroy', 'async-local-storage'],
asyncMethod: ['callbacks', 'async'],
n: [1e6]
});
Expand Down Expand Up @@ -102,6 +103,35 @@ function buildDestroy(getServe) {
}
}

function buildAsyncLocalStorage(getServe) {
const asyncLocalStorage = new AsyncLocalStorage();
const server = createServer((req, res) => {
asyncLocalStorage.runSyncAndReturn(() => {
getServe(getCLS, setCLS)(req, res);
});
});

return {
server,
close
};

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

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

function close() {
asyncLocalStorage.disable();
server.close();
}
}

function getServeAwait(getCLS, setCLS) {
return async function serve(req, res) {
setCLS(Math.random());
Expand All @@ -126,7 +156,8 @@ function getServeCallbacks(getCLS, setCLS) {

const types = {
'async-resource': buildCurrentResource,
'destroy': buildDestroy
'destroy': buildDestroy,
'async-local-storage': buildAsyncLocalStorage
};

const asyncMethods = {
Expand Down
287 changes: 287 additions & 0 deletions doc/api/async_hooks.md
Expand Up @@ -859,6 +859,293 @@ for (let i = 0; i < 10; i++) {
}
```

## Class: `AsyncLocalStorage`
<!-- YAML
added: REPLACEME
-->
vdeturckheim marked this conversation as resolved.
Show resolved Hide resolved

This class is used to create asynchronous state within callbacks and promise
chains. It allows storing data throughout the lifetime of a web request
or any other asynchronous duration. It is similar to thread-local storage
in other languages.

The following example builds a logger that will always know the current HTTP
request and uses it to display enhanced logs without needing to explicitly
provide the current HTTP request to it.

```js
const { AsyncLocalStorage } = require('async_hooks');
const http = require('http');

const kReq = 'CURRENT_REQUEST';
const asyncLocalStorage = new AsyncLocalStorage();

function log(...args) {
const store = asyncLocalStorage.getStore();
// Make sure the store exists and it contains a request.
if (store && store.has(kReq)) {
const req = store.get(kReq);
// Prints `GET /items ERR could not do something
console.log(req.method, req.url, ...args);
} else {
console.log(...args);
}
}

http.createServer((request, response) => {
asyncLocalStorage.run(() => {
const store = asyncLocalStorage.getStore();
store.set(kReq, request);
someAsyncOperation((err, result) => {
if (err) {
log('ERR', err.message);
}
});
});
})
.listen(8080);
```

When having multiple instances of `AsyncLocalStorage`, they are independent
from each other. It is safe to instantiate this class multiple times.

### `new AsyncLocalStorage()`
<!-- YAML
added: REPLACEME
-->

vdeturckheim marked this conversation as resolved.
Show resolved Hide resolved
Creates a new instance of `AsyncLocalStorage`. Store is only provided within a
`run` or a `runSyncAndReturn` method call.

vdeturckheim marked this conversation as resolved.
Show resolved Hide resolved
### `asyncLocalStorage.disable()`
<!-- YAML
added: REPLACEME
-->

This method disables the instance of `AsyncLocalStorage`. All subsequent calls
to `asyncLocalStorage.getStore()` will return `undefined` until
`asyncLocalStorage.run()` or `asyncLocalStorage.runSyncAndReturn()`
is called again.

When calling `asyncLocalStorage.disable()`, all current contexts linked to the
instance will be exited.

Calling `asyncLocalStorage.disable()` is required before the
`asyncLocalStorage` can be garbage collected. This does not apply to stores
provided by the `asyncLocalStorage`, as those objects are garbage collected
along with the corresponding async resources.

This method is to be used when the `asyncLocalStorage` is not in use anymore
in the current process.

### `asyncLocalStorage.getStore()`
<!-- YAML
added: REPLACEME
-->

* Returns: {Map}

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])`
<!-- YAML
added: REPLACEME
-->

* `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.

The callback will be ran asynchronously. Optionally, arguments can be passed
to the function. They will be passed to the callback function.

If an error is thrown by the callback function, it will not be caught by
a `try/catch` block as the callback is ran in a new asynchronous resource.
Also, the stacktrace will be impacted by the asynchronous call.

Example:

```js
asyncLocalStorage.run(() => {
asyncLocalStorage.getStore(); // Returns a Map
someAsyncOperation(() => {
asyncLocalStorage.getStore(); // Returns the same Map
});
});
asyncLocalStorage.getStore(); // Returns undefined
```

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

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

vdeturckheim marked this conversation as resolved.
Show resolved Hide resolved
Calling `asyncLocalStorage.exit(callback)` will create a new asynchronous
context.
Within the callback function and the asynchronous operations from the callback,
`asyncLocalStorage.getStore()` will return `undefined`.

The callback will be ran asynchronously. Optionally, arguments can be passed
to the function. They will be passed to the callback function.

If an error is thrown by the callback function, it will not be caught by
a `try/catch` block as the callback is ran in a new asynchronous resource.
Also, the stacktrace will be impacted by the asynchronous call.

Example:

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

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

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

This methods runs a function synchronously within a context and return its
return value. The store is not accessible outside of the callback function or
the asynchronous operations created within the callback.

Optionally, arguments can be passed to the function. They will be passed to
the callback function.

If the callback function throws an error, it will be thrown by
`runSyncAndReturn` too. The stacktrace will not be impacted by this call and
the context will be exited.

Example:

```js
try {
asyncLocalStorage.runSyncAndReturn(() => {
asyncLocalStorage.getStore(); // Returns a Map
throw new Error();
});
} catch (e) {
asyncLocalStorage.getStore(); // Returns undefined
// The error will be caught here
}
```

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

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

This methods runs a function synchronously outside of a context and return its
return value. The store is not accessible within the callback function or
the asynchronous operations created within the callback.

Optionally, arguments can be passed to the function. They will be passed to
the callback function.

If the callback function throws an error, it will be thrown by
`exitSyncAndReturn` too. The stacktrace will not be impacted by this call and
the context will be re-entered.

Example:

```js
// Within a call to run or runSyncAndReturn
try {
asyncLocalStorage.getStore(); // Returns a Map
asyncLocalStorage.exitSyncAndReturn(() => {
asyncLocalStorage.getStore(); // Returns undefined
throw new Error();
});
} catch (e) {
asyncLocalStorage.getStore(); // Returns the same Map
// The error will be caught here
}
```

### Choosing between `run` and `runSyncAndReturn`

#### When to choose `run`

`run` is asynchronous. It is called with a callback function that
runs within a new asynchronous call. This is the most explicit behavior as
everything that is executed within the callback of `run` (including further
asynchronous operations) will have access to the store.

If an instance of `AsyncLocalStorage` is used for error management (for
instance, with `process.setUncaughtExceptionCaptureCallback`), only
exceptions thrown in the scope of the callback function will be associated
with the context.

This method is the safest as it provides strong scoping and consistent
behavior.

It cannot be promisified using `util.promisify`. If needed, the `Promise`
constructor can be used:

```js
new Promise((resolve, reject) => {
asyncLocalStorage.run(() => {
someFunction((err, result) => {
if (err) {
return reject(err);
}
return resolve(result);
});
});
});
```

#### When to choose `runSyncAndReturn`

`runSyncAndReturn` is synchronous. The callback function will be executed
synchronously and its return value will be returned by `runSyncAndReturn`.
The store will only be accessible from within the callback
function and the asynchronous operations created within this scope.
If the callback throws an error, `runSyncAndReturn` will throw it and it will
not be associated with the context.

This method provides good scoping while being synchronous.

#### Usage with `async/await`

If, within an async function, only one `await` call is to run within a context,
the following pattern should be used:

```js
async function fn() {
await asyncLocalStorage.runSyncAndReturn(() => {
asyncLocalStorage.getStore().set('key', value);
return foo(); // The return value of foo will be awaited
});
}
```

In this example, the store is only available in the callback function and the
functions called by `foo`. Outside of `runSyncAndReturn`, calling `getStore`
will return `undefined`.

[`after` callback]: #async_hooks_after_asyncid
[`before` callback]: #async_hooks_before_asyncid
[`destroy` callback]: #async_hooks_destroy_asyncid
Expand Down