-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
async_hooks: add AsyncLocal class #27172
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
Changes from 1 commit
2afe57a
a80e811
9ec3977
1cef6c3
593552e
dfe9477
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
This introduces a new API to provide asynchronous storage via a new AsyncLocal class. Besides setting a value which is passed along asynchronous invocations it allows to register a callback to get informed about changes of the current value. The implementation is based on async_hooks but it doesn't expose internals like execution Ids, resources or the hooks itself. Naming and implementation is inspired by .NET AsyncLocal.
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,8 @@ const { | |
|
|
||
| const { | ||
| ERR_ASYNC_CALLBACK, | ||
| ERR_INVALID_ASYNC_ID | ||
| ERR_INVALID_ASYNC_ID, | ||
| ERR_INVALID_ARG_TYPE | ||
| } = require('internal/errors').codes; | ||
| const { validateString } = require('internal/validators'); | ||
| const internal_async_hooks = require('internal/async_hooks'); | ||
|
|
@@ -200,6 +201,123 @@ class AsyncResource { | |
| } | ||
|
|
||
|
|
||
| // AsyncLocal // | ||
|
|
||
| const kStack = Symbol('stack'); | ||
| const kIsFirst = Symbol('is-first'); | ||
| const kMap = Symbol('map'); | ||
| const kOnChangedCb = Symbol('on-changed-cb'); | ||
| const kHooks = Symbol('hooks'); | ||
| const kSet = Symbol('set'); | ||
|
|
||
| class AsyncLocal { | ||
| constructor(options = {}) { | ||
| if (typeof options !== 'object' || options === null) | ||
| throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); | ||
|
|
||
| const { onChangedCb = null } = options; | ||
| if (onChangedCb !== null && typeof onChangedCb !== 'function') | ||
| throw new ERR_INVALID_ARG_TYPE('options.onChangedCb', | ||
| 'function', | ||
| onChangedCb); | ||
|
|
||
| this[kOnChangedCb] = onChangedCb; | ||
| this[kMap] = new Map(); | ||
|
|
||
| const fns = { | ||
| init: (asyncId, type, triggerAsyncId, resource) => { | ||
| // Propagate value from current id to new (execution graph) | ||
| const value = this[kMap].get(executionAsyncId()); | ||
| if (value) | ||
| this[kMap].set(asyncId, value); | ||
| }, | ||
|
|
||
| destroy: (asyncId) => this[kSet](asyncId, null), | ||
| }; | ||
|
|
||
| if (this[kOnChangedCb]) { | ||
| // Change notification requires to keep a stack of async local values | ||
| this[kStack] = []; | ||
| // Indicates that first value was stored (before callback "missing") | ||
| this[kIsFirst] = true; | ||
|
|
||
| // Use before/after hooks to signal changes because of execution | ||
| fns.before = (asyncId) => { | ||
| const stack = this[kStack]; | ||
| const cVal = this[kMap].get(asyncId); | ||
| const pVal = stack[stack.length - 1]; | ||
| stack.push(pVal); | ||
|
Flarna marked this conversation as resolved.
Outdated
|
||
| if (cVal !== pVal) | ||
| this[kOnChangedCb](pVal, cVal, true); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This invocation is very risky. User callbask should be called outside of async hooks.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, then it will not work as moving it outside of async hooks would require something like nextTick which is a new async operation. As a result the signaled values and the real values don't match anymore.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this cannot be made safe, I don't think supporting this "onchanged" behavior is compatible for the goal of "ease of use" that this API is supposed to have.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will improve docs in this, add a check to catch recursion cases (e.g. onChangedCb sets a new value) and handle exceptions thrown by onChangedCb. Hopefully this renders this callback as save as all the other callbacks which can be registered in node core. |
||
| }; | ||
|
|
||
| fns.after = (asyncId) => { | ||
| const stack = this[kStack]; | ||
| const pVal = this[kMap].get(asyncId); | ||
| stack.pop(); | ||
| const cVal = stack[stack.length - 1]; | ||
| if (cVal !== pVal) | ||
| this[kOnChangedCb](pVal, cVal, true); | ||
| }; | ||
| } | ||
| this[kHooks] = createHook(fns); | ||
| } | ||
|
|
||
| set value(val) { | ||
| val = val === null ? undefined : val; | ||
| const id = executionAsyncId(); | ||
| const onChangedCb = this[kOnChangedCb]; | ||
| let pVal; | ||
| if (onChangedCb) | ||
| pVal = this[kMap].get(id); | ||
|
|
||
| this[kSet](id, val); | ||
|
|
||
| if (onChangedCb && pVal !== val) | ||
| onChangedCb(pVal, val, false); | ||
| } | ||
|
|
||
| get value() { | ||
| return this[kMap].get(executionAsyncId()); | ||
| } | ||
|
|
||
| [kSet](id, val) { | ||
| const map = this[kMap]; | ||
|
|
||
| if (val == null) { | ||
| map.delete(id); | ||
| if (map.size === 0) | ||
| this[kHooks].disable(); | ||
|
|
||
| if (this[kOnChangedCb]) { | ||
| if (map.size === 0) { | ||
| // Hooks have been disabled so next set is the first one | ||
| this[kStack] = []; | ||
| this[kIsFirst] = true; | ||
| } else { | ||
| const stack = this[kStack]; | ||
| stack[stack.length - 1] = undefined; | ||
| } | ||
| } | ||
| } else { | ||
| map.set(id, val); | ||
| if (map.size === 1) | ||
| this[kHooks].enable(); | ||
|
|
||
| if (this[kOnChangedCb]) { | ||
| const stack = this[kStack]; | ||
| if (this[kIsFirst]) { | ||
| // First value set => "simulate" before hook | ||
| this[kIsFirst] = false; | ||
| stack.push(val); | ||
| } else { | ||
| stack[stack.length - 1] = val; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Placing all exports down here because the exported classes won't export | ||
| // otherwise. | ||
| module.exports = { | ||
|
|
@@ -209,4 +327,6 @@ module.exports = { | |
| triggerAsyncId, | ||
| // Embedder API | ||
| AsyncResource, | ||
| // CLS API | ||
| AsyncLocal | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.