Skip to content

Commit 9c70292

Browse files
committed
async_hooks: introduce async-context API
Adding AsyncLocalStorage class to async_hooks module. This API provide a simple CLS-like set of features. Co-authored-by: Andrey Pechkurov <[email protected]> PR-URL: #26540 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Stephen Belanger <[email protected]> Reviewed-By: Gireesh Punathil <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
1 parent 72b6cea commit 9c70292

13 files changed

+667
-3
lines changed

benchmark/async_hooks/async-resource-vs-destroy.js

+34-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ const common = require('../common.js');
88
const {
99
createHook,
1010
executionAsyncResource,
11-
executionAsyncId
11+
executionAsyncId,
12+
AsyncLocalStorage
1213
} = require('async_hooks');
1314
const { createServer } = require('http');
1415

@@ -18,7 +19,7 @@ const connections = 500;
1819
const path = '/';
1920

2021
const bench = common.createBenchmark(main, {
21-
type: ['async-resource', 'destroy'],
22+
type: ['async-resource', 'destroy', 'async-local-storage'],
2223
asyncMethod: ['callbacks', 'async'],
2324
n: [1e6]
2425
});
@@ -102,6 +103,35 @@ function buildDestroy(getServe) {
102103
}
103104
}
104105

106+
function buildAsyncLocalStorage(getServe) {
107+
const asyncLocalStorage = new AsyncLocalStorage();
108+
const server = createServer((req, res) => {
109+
asyncLocalStorage.runSyncAndReturn(() => {
110+
getServe(getCLS, setCLS)(req, res);
111+
});
112+
});
113+
114+
return {
115+
server,
116+
close
117+
};
118+
119+
function getCLS() {
120+
const store = asyncLocalStorage.getStore();
121+
return store.get('store');
122+
}
123+
124+
function setCLS(state) {
125+
const store = asyncLocalStorage.getStore();
126+
store.set('store', state);
127+
}
128+
129+
function close() {
130+
asyncLocalStorage.disable();
131+
server.close();
132+
}
133+
}
134+
105135
function getServeAwait(getCLS, setCLS) {
106136
return async function serve(req, res) {
107137
setCLS(Math.random());
@@ -126,7 +156,8 @@ function getServeCallbacks(getCLS, setCLS) {
126156

127157
const types = {
128158
'async-resource': buildCurrentResource,
129-
'destroy': buildDestroy
159+
'destroy': buildDestroy,
160+
'async-local-storage': buildAsyncLocalStorage
130161
};
131162

132163
const asyncMethods = {

doc/api/async_hooks.md

+287
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,293 @@ for (let i = 0; i < 10; i++) {
859859
}
860860
```
861861

862+
## Class: `AsyncLocalStorage`
863+
<!-- YAML
864+
added: REPLACEME
865+
-->
866+
867+
This class is used to create asynchronous state within callbacks and promise
868+
chains. It allows storing data throughout the lifetime of a web request
869+
or any other asynchronous duration. It is similar to thread-local storage
870+
in other languages.
871+
872+
The following example builds a logger that will always know the current HTTP
873+
request and uses it to display enhanced logs without needing to explicitly
874+
provide the current HTTP request to it.
875+
876+
```js
877+
const { AsyncLocalStorage } = require('async_hooks');
878+
const http = require('http');
879+
880+
const kReq = 'CURRENT_REQUEST';
881+
const asyncLocalStorage = new AsyncLocalStorage();
882+
883+
function log(...args) {
884+
const store = asyncLocalStorage.getStore();
885+
// Make sure the store exists and it contains a request.
886+
if (store && store.has(kReq)) {
887+
const req = store.get(kReq);
888+
// Prints `GET /items ERR could not do something
889+
console.log(req.method, req.url, ...args);
890+
} else {
891+
console.log(...args);
892+
}
893+
}
894+
895+
http.createServer((request, response) => {
896+
asyncLocalStorage.run(() => {
897+
const store = asyncLocalStorage.getStore();
898+
store.set(kReq, request);
899+
someAsyncOperation((err, result) => {
900+
if (err) {
901+
log('ERR', err.message);
902+
}
903+
});
904+
});
905+
})
906+
.listen(8080);
907+
```
908+
909+
When having multiple instances of `AsyncLocalStorage`, they are independent
910+
from each other. It is safe to instantiate this class multiple times.
911+
912+
### `new AsyncLocalStorage()`
913+
<!-- YAML
914+
added: REPLACEME
915+
-->
916+
917+
Creates a new instance of `AsyncLocalStorage`. Store is only provided within a
918+
`run` or a `runSyncAndReturn` method call.
919+
920+
### `asyncLocalStorage.disable()`
921+
<!-- YAML
922+
added: REPLACEME
923+
-->
924+
925+
This method disables the instance of `AsyncLocalStorage`. All subsequent calls
926+
to `asyncLocalStorage.getStore()` will return `undefined` until
927+
`asyncLocalStorage.run()` or `asyncLocalStorage.runSyncAndReturn()`
928+
is called again.
929+
930+
When calling `asyncLocalStorage.disable()`, all current contexts linked to the
931+
instance will be exited.
932+
933+
Calling `asyncLocalStorage.disable()` is required before the
934+
`asyncLocalStorage` can be garbage collected. This does not apply to stores
935+
provided by the `asyncLocalStorage`, as those objects are garbage collected
936+
along with the corresponding async resources.
937+
938+
This method is to be used when the `asyncLocalStorage` is not in use anymore
939+
in the current process.
940+
941+
### `asyncLocalStorage.getStore()`
942+
<!-- YAML
943+
added: REPLACEME
944+
-->
945+
946+
* Returns: {Map}
947+
948+
This method returns the current store.
949+
If this method is called outside of an asynchronous context initialized by
950+
calling `asyncLocalStorage.run` or `asyncLocalStorage.runAndReturn`, it will
951+
return `undefined`.
952+
953+
### `asyncLocalStorage.run(callback[, ...args])`
954+
<!-- YAML
955+
added: REPLACEME
956+
-->
957+
958+
* `callback` {Function}
959+
* `...args` {any}
960+
961+
Calling `asyncLocalStorage.run(callback)` will create a new asynchronous
962+
context.
963+
Within the callback function and the asynchronous operations from the callback,
964+
`asyncLocalStorage.getStore()` will return an instance of `Map` known as
965+
"the store". This store will be persistent through the following
966+
asynchronous calls.
967+
968+
The callback will be ran asynchronously. Optionally, arguments can be passed
969+
to the function. They will be passed to the callback function.
970+
971+
If an error is thrown by the callback function, it will not be caught by
972+
a `try/catch` block as the callback is ran in a new asynchronous resource.
973+
Also, the stacktrace will be impacted by the asynchronous call.
974+
975+
Example:
976+
977+
```js
978+
asyncLocalStorage.run(() => {
979+
asyncLocalStorage.getStore(); // Returns a Map
980+
someAsyncOperation(() => {
981+
asyncLocalStorage.getStore(); // Returns the same Map
982+
});
983+
});
984+
asyncLocalStorage.getStore(); // Returns undefined
985+
```
986+
987+
### `asyncLocalStorage.exit(callback[, ...args])`
988+
<!-- YAML
989+
added: REPLACEME
990+
-->
991+
992+
* `callback` {Function}
993+
* `...args` {any}
994+
995+
Calling `asyncLocalStorage.exit(callback)` will create a new asynchronous
996+
context.
997+
Within the callback function and the asynchronous operations from the callback,
998+
`asyncLocalStorage.getStore()` will return `undefined`.
999+
1000+
The callback will be ran asynchronously. Optionally, arguments can be passed
1001+
to the function. They will be passed to the callback function.
1002+
1003+
If an error is thrown by the callback function, it will not be caught by
1004+
a `try/catch` block as the callback is ran in a new asynchronous resource.
1005+
Also, the stacktrace will be impacted by the asynchronous call.
1006+
1007+
Example:
1008+
1009+
```js
1010+
asyncLocalStorage.run(() => {
1011+
asyncLocalStorage.getStore(); // Returns a Map
1012+
asyncLocalStorage.exit(() => {
1013+
asyncLocalStorage.getStore(); // Returns undefined
1014+
});
1015+
asyncLocalStorage.getStore(); // Returns the same Map
1016+
});
1017+
```
1018+
1019+
### `asyncLocalStorage.runSyncAndReturn(callback[, ...args])`
1020+
<!-- YAML
1021+
added: REPLACEME
1022+
-->
1023+
1024+
* `callback` {Function}
1025+
* `...args` {any}
1026+
1027+
This methods runs a function synchronously within a context and return its
1028+
return value. The store is not accessible outside of the callback function or
1029+
the asynchronous operations created within the callback.
1030+
1031+
Optionally, arguments can be passed to the function. They will be passed to
1032+
the callback function.
1033+
1034+
If the callback function throws an error, it will be thrown by
1035+
`runSyncAndReturn` too. The stacktrace will not be impacted by this call and
1036+
the context will be exited.
1037+
1038+
Example:
1039+
1040+
```js
1041+
try {
1042+
asyncLocalStorage.runSyncAndReturn(() => {
1043+
asyncLocalStorage.getStore(); // Returns a Map
1044+
throw new Error();
1045+
});
1046+
} catch (e) {
1047+
asyncLocalStorage.getStore(); // Returns undefined
1048+
// The error will be caught here
1049+
}
1050+
```
1051+
1052+
### `asyncLocalStorage.exitSyncAndReturn(callback[, ...args])`
1053+
<!-- YAML
1054+
added: REPLACEME
1055+
-->
1056+
1057+
* `callback` {Function}
1058+
* `...args` {any}
1059+
1060+
This methods runs a function synchronously outside of a context and return its
1061+
return value. The store is not accessible within the callback function or
1062+
the asynchronous operations created within the callback.
1063+
1064+
Optionally, arguments can be passed to the function. They will be passed to
1065+
the callback function.
1066+
1067+
If the callback function throws an error, it will be thrown by
1068+
`exitSyncAndReturn` too. The stacktrace will not be impacted by this call and
1069+
the context will be re-entered.
1070+
1071+
Example:
1072+
1073+
```js
1074+
// Within a call to run or runSyncAndReturn
1075+
try {
1076+
asyncLocalStorage.getStore(); // Returns a Map
1077+
asyncLocalStorage.exitSyncAndReturn(() => {
1078+
asyncLocalStorage.getStore(); // Returns undefined
1079+
throw new Error();
1080+
});
1081+
} catch (e) {
1082+
asyncLocalStorage.getStore(); // Returns the same Map
1083+
// The error will be caught here
1084+
}
1085+
```
1086+
1087+
### Choosing between `run` and `runSyncAndReturn`
1088+
1089+
#### When to choose `run`
1090+
1091+
`run` is asynchronous. It is called with a callback function that
1092+
runs within a new asynchronous call. This is the most explicit behavior as
1093+
everything that is executed within the callback of `run` (including further
1094+
asynchronous operations) will have access to the store.
1095+
1096+
If an instance of `AsyncLocalStorage` is used for error management (for
1097+
instance, with `process.setUncaughtExceptionCaptureCallback`), only
1098+
exceptions thrown in the scope of the callback function will be associated
1099+
with the context.
1100+
1101+
This method is the safest as it provides strong scoping and consistent
1102+
behavior.
1103+
1104+
It cannot be promisified using `util.promisify`. If needed, the `Promise`
1105+
constructor can be used:
1106+
1107+
```js
1108+
new Promise((resolve, reject) => {
1109+
asyncLocalStorage.run(() => {
1110+
someFunction((err, result) => {
1111+
if (err) {
1112+
return reject(err);
1113+
}
1114+
return resolve(result);
1115+
});
1116+
});
1117+
});
1118+
```
1119+
1120+
#### When to choose `runSyncAndReturn`
1121+
1122+
`runSyncAndReturn` is synchronous. The callback function will be executed
1123+
synchronously and its return value will be returned by `runSyncAndReturn`.
1124+
The store will only be accessible from within the callback
1125+
function and the asynchronous operations created within this scope.
1126+
If the callback throws an error, `runSyncAndReturn` will throw it and it will
1127+
not be associated with the context.
1128+
1129+
This method provides good scoping while being synchronous.
1130+
1131+
#### Usage with `async/await`
1132+
1133+
If, within an async function, only one `await` call is to run within a context,
1134+
the following pattern should be used:
1135+
1136+
```js
1137+
async function fn() {
1138+
await asyncLocalStorage.runSyncAndReturn(() => {
1139+
asyncLocalStorage.getStore().set('key', value);
1140+
return foo(); // The return value of foo will be awaited
1141+
});
1142+
}
1143+
```
1144+
1145+
In this example, the store is only available in the callback function and the
1146+
functions called by `foo`. Outside of `runSyncAndReturn`, calling `getStore`
1147+
will return `undefined`.
1148+
8621149
[`after` callback]: #async_hooks_after_asyncid
8631150
[`before` callback]: #async_hooks_before_asyncid
8641151
[`destroy` callback]: #async_hooks_destroy_asyncid

0 commit comments

Comments
 (0)