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 702eeb2
Show file tree
Hide file tree
Showing 25 changed files with 824 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
125 changes: 20 additions & 105 deletions lib/async_hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const {
FunctionPrototypeBind,
NumberIsSafeInteger,
ObjectDefineProperties,
ObjectIs,
ReflectApply,
Symbol,
ObjectFreeze,
Expand All @@ -30,6 +29,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 +160,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 +180,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 +206,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 +280,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() {
return hasAsyncContextFrame ?
require('internal/async_local_storage/native') :
require('internal/async_local_storage/async_hooks');
},
createHook,
executionAsyncId,
triggerAsyncId,
Expand Down
117 changes: 117 additions & 0 deletions lib/internal/async_local_storage/async_hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
'use strict';

const {
ArrayPrototypeIndexOf,
ArrayPrototypePush,
ArrayPrototypeSplice,
ObjectIs,
ReflectApply,
Symbol,
} = primordials;

const {
AsyncResource,
createHook,
executionAsyncResource,
} = require('async_hooks');

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
const index = ArrayPrototypeIndexOf(storageList, this);
ArrayPrototypeSplice(storageList, index, 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 702eeb2

Please sign in to comment.