Skip to content

Commit 16e8b11

Browse files
vdeturckheimtargos
authored andcommitted
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 e204dba commit 16e8b11

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
@@ -866,6 +866,293 @@ for (let i = 0; i < 10; i++) {
866866
}
867867
```
868868

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

0 commit comments

Comments
 (0)