Skip to content

Commit

Permalink
lib: rewrite AsyncLocalStorage without async_hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
Stephen Belanger committed Jul 28, 2023
1 parent 48345d0 commit 5a7b995
Show file tree
Hide file tree
Showing 25 changed files with 823 additions and 126 deletions.
8 changes: 8 additions & 0 deletions configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,12 @@
default=None,
help='do not install the bundled Corepack')

parser.add_argument('--with-native-als',
action='store_true',
dest='with_native_als',
default=None,
help='use native AsyncLocalStorage')

# Dummy option for backwards compatibility
parser.add_argument('--without-report',
action='store_true',
Expand Down Expand Up @@ -1237,6 +1243,8 @@ def configure_node(o):
o['default_configuration'] = 'Debug' if options.debug else 'Release'
o['variables']['error_on_warn'] = b(options.error_on_warn)

o['variables']['node_use_native_als'] = b(options.with_native_als)

host_arch = host_arch_win() if os.name == 'nt' else host_arch_cc()
target_arch = options.dest_cpu or host_arch
# ia32 is preferred by the build tools (GYP) over x86 even if we prefer the latter
Expand Down
124 changes: 20 additions & 104 deletions lib/async_hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const {
} = require('internal/validators');
const internal_async_hooks = require('internal/async_hooks');

const { AsyncContextFrame } = internalBinding('async_context_frame');
const hasAsyncContextFrame = typeof AsyncContextFrame === 'function';

// Get functions
// For userland AsyncResources, make sure to emit a destroy event when the
// resource gets gced.
Expand Down Expand Up @@ -158,6 +161,7 @@ function createHook(fns) {
// Embedder API //

const destroyedSymbol = Symbol('destroyed');
const contextFrameSymbol = Symbol('context_frame');

class AsyncResource {
constructor(type, opts = kEmptyObject) {
Expand All @@ -177,6 +181,8 @@ class AsyncResource {
throw new ERR_INVALID_ASYNC_ID('triggerAsyncId', triggerAsyncId);
}

this[contextFrameSymbol] = hasAsyncContextFrame && AsyncContextFrame.current();

const asyncId = newAsyncId();
this[async_id_symbol] = asyncId;
this[trigger_async_id_symbol] = triggerAsyncId;
Expand All @@ -201,12 +207,17 @@ class AsyncResource {
const asyncId = this[async_id_symbol];
emitBefore(asyncId, this[trigger_async_id_symbol], this);

const contextFrame = this[contextFrameSymbol];
let prior;
if (hasAsyncContextFrame) {
prior = AsyncContextFrame.exchange(contextFrame);
}
try {
const ret =
ReflectApply(fn, thisArg, args);

return ret;
return ReflectApply(fn, thisArg, args);
} finally {
if (hasAsyncContextFrame) {
AsyncContextFrame.exchange(prior);
}
if (hasAsyncIdStack())
emitAfter(asyncId);
}
Expand Down Expand Up @@ -270,110 +281,15 @@ class AsyncResource {
}
}

const storageList = [];
const storageHook = createHook({
init(asyncId, type, triggerAsyncId, resource) {
const currentResource = executionAsyncResource();
// Value of currentResource is always a non null object
for (let i = 0; i < storageList.length; ++i) {
storageList[i]._propagate(resource, currentResource, type);
}
},
});

class AsyncLocalStorage {
constructor() {
this.kResourceStore = Symbol('kResourceStore');
this.enabled = false;
}

static bind(fn) {
return AsyncResource.bind(fn);
}

static snapshot() {
return AsyncLocalStorage.bind((cb, ...args) => cb(...args));
}

disable() {
if (this.enabled) {
this.enabled = false;
// If this.enabled, the instance must be in storageList
ArrayPrototypeSplice(storageList,
ArrayPrototypeIndexOf(storageList, this), 1);
if (storageList.length === 0) {
storageHook.disable();
}
}
}

_enable() {
if (!this.enabled) {
this.enabled = true;
ArrayPrototypePush(storageList, this);
storageHook.enable();
}
}

// Propagate the context from a parent resource to a child one
_propagate(resource, triggerResource, type) {
const store = triggerResource[this.kResourceStore];
if (this.enabled) {
resource[this.kResourceStore] = store;
}
}

enterWith(store) {
this._enable();
const resource = executionAsyncResource();
resource[this.kResourceStore] = store;
}

run(store, callback, ...args) {
// Avoid creation of an AsyncResource if store is already active
if (ObjectIs(store, this.getStore())) {
return ReflectApply(callback, null, args);
}

this._enable();

const resource = executionAsyncResource();
const oldStore = resource[this.kResourceStore];

resource[this.kResourceStore] = store;

try {
return ReflectApply(callback, null, args);
} finally {
resource[this.kResourceStore] = oldStore;
}
}

exit(callback, ...args) {
if (!this.enabled) {
return ReflectApply(callback, null, args);
}
this.disable();
try {
return ReflectApply(callback, null, args);
} finally {
this._enable();
}
}

getStore() {
if (this.enabled) {
const resource = executionAsyncResource();
return resource[this.kResourceStore];
}
}
}

// Placing all exports down here because the exported classes won't export
// otherwise.
module.exports = {
// Public API
AsyncLocalStorage,
get AsyncLocalStorage () {

Check failure on line 288 in lib/async_hooks.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Unexpected space before function parentheses
return hasAsyncContextFrame
? require('internal/async_local_storage/native')

Check failure on line 290 in lib/async_hooks.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

'?' should be placed at the end of the line
: require('internal/async_local_storage/async_hooks');

Check failure on line 291 in lib/async_hooks.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

':' should be placed at the end of the line
},
createHook,
executionAsyncId,
triggerAsyncId,
Expand Down
116 changes: 116 additions & 0 deletions lib/internal/async_local_storage/async_hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
'use strict';

const {
ArrayPrototypeIndexOf,
ArrayPrototypePush,
ArrayPrototypeSplice,
ObjectIs,
ReflectApply

Check failure on line 8 in lib/internal/async_local_storage/async_hooks.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing trailing comma
} = primordials;

const {
AsyncResource,
createHook,
executionAsyncResource

Check failure on line 14 in lib/internal/async_local_storage/async_hooks.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing trailing comma
} = require('async_hooks');

const storageList = [];
const storageHook = createHook({
init (asyncId, type, triggerAsyncId, resource) {

Check failure on line 19 in lib/internal/async_local_storage/async_hooks.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Unexpected space before function parentheses
const currentResource = executionAsyncResource();
// Value of currentResource is always a non null object
for (let i = 0; i < storageList.length; ++i) {
storageList[i]._propagate(resource, currentResource, type);
}
},
});

class AsyncLocalStorage {
constructor () {

Check failure on line 29 in lib/internal/async_local_storage/async_hooks.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Unexpected space before function parentheses
this.kResourceStore = Symbol('kResourceStore');

Check failure on line 30 in lib/internal/async_local_storage/async_hooks.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Use `const { Symbol } = primordials;` instead of the global
this.enabled = false;
}

static bind (fn) {

Check failure on line 34 in lib/internal/async_local_storage/async_hooks.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Unexpected space before function parentheses
return AsyncResource.bind(fn);
}

static snapshot () {
return AsyncLocalStorage.bind((cb, ...args) => cb(...args));
}

disable () {
if (this.enabled) {
this.enabled = false;
// If this.enabled, the instance must be in storageList
ArrayPrototypeSplice(storageList,
ArrayPrototypeIndexOf(storageList, this), 1);
if (storageList.length === 0) {
storageHook.disable();
}
}
}

_enable () {
if (!this.enabled) {
this.enabled = true;
ArrayPrototypePush(storageList, this);
storageHook.enable();
}
}

// Propagate the context from a parent resource to a child one
_propagate (resource, triggerResource, type) {
const store = triggerResource[this.kResourceStore];
if (this.enabled) {
resource[this.kResourceStore] = store;
}
}

enterWith (store) {
this._enable();
const resource = executionAsyncResource();
resource[this.kResourceStore] = store;
}

run (store, callback, ...args) {
// Avoid creation of an AsyncResource if store is already active
if (ObjectIs(store, this.getStore())) {
return ReflectApply(callback, null, args);
}

this._enable();

const resource = executionAsyncResource();
const oldStore = resource[this.kResourceStore];

resource[this.kResourceStore] = store;

try {
return ReflectApply(callback, null, args);
} finally {
resource[this.kResourceStore] = oldStore;
}
}

exit (callback, ...args) {
if (!this.enabled) {
return ReflectApply(callback, null, args);
}
this.disable();
try {
return ReflectApply(callback, null, args);
} finally {
this._enable();
}
}

getStore () {
if (this.enabled) {
const resource = executionAsyncResource();
return resource[this.kResourceStore];
}
}
}

module.exports = AsyncLocalStorage;
63 changes: 63 additions & 0 deletions lib/internal/async_local_storage/native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use strict';

const {
FunctionPrototypeBind,
ReflectApply
} = primordials;

const { validateFunction } = require('internal/validators');

const { AsyncContextFrame } = internalBinding('async_context_frame');

class AsyncLocalStorage {
static bind (fn) {
validateFunction(fn, 'fn');
const run = this.snapshot()
return function bound (...args) {
return run.call(this, fn, ...args);
};
}

static snapshot () {
const frame = AsyncContextFrame.current();
return function runSnapshot (fn, ...args) {
const bound = FunctionPrototypeBind(fn, this);
const prior = AsyncContextFrame.exchange(frame);
try {
return ReflectApply(bound, undefined, args);
} finally {
AsyncContextFrame.exchange(prior);
}
};
}

disable () {
AsyncContextFrame.disable(this);
}

enterWith (data) {
const frame = new AsyncContextFrame(this, data);
AsyncContextFrame.exchange(frame);
}

run (data, fn, ...args) {
const frame = new AsyncContextFrame(this, data);
const prior = AsyncContextFrame.exchange(frame);

try {
return ReflectApply(fn, undefined, args);
} finally {
AsyncContextFrame.exchange(prior);
}
}

exit (fn, ...args) {
return ReflectApply(this.run, this, [undefined, fn, ...args]);
}

getStore () {
return AsyncContextFrame.current()?.get(this);
}
}

module.exports = AsyncLocalStorage;
Loading

0 comments on commit 5a7b995

Please sign in to comment.