Skip to content

Commit

Permalink
async_hooks: introduce async-context API
Browse files Browse the repository at this point in the history
Adding AsyncLocalStorage class to async_hooks
 module.
This API provide a simple CLS-like set
of features.

Co-authored-by: Andrey Pechkurov <apechkurov@gmail.com>

PR-URL: #26540
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
  • Loading branch information
vdeturckheim authored and targos committed Apr 28, 2020
1 parent e204dba commit 16e8b11
Show file tree
Hide file tree
Showing 13 changed files with 667 additions and 3 deletions.
37 changes: 34 additions & 3 deletions benchmark/async_hooks/async-resource-vs-destroy.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,293 @@ for (let i = 0; i < 10; i++) {
}
```

## Class: `AsyncLocalStorage`
<!-- YAML
added: REPLACEME
-->

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
-->

Creates a new instance of `AsyncLocalStorage`. Store is only provided within a
`run` or a `runSyncAndReturn` method call.

### `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}

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
Loading

0 comments on commit 16e8b11

Please sign in to comment.