diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index 7f50ca2dbf39c4..9f28e1e2148089 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -1103,6 +1103,20 @@ for the sync error and one for the async error. ### Built-in Channels +#### Async Context Frame + +> Stability: 1 - Experimental + +`async_context_frame.set` + +* `frame` {Object} + +Emitted when the async context frame for the current execution context is set. +This happens in response to [`AsyncLocalStorage.enterWith`][], +[`AsyncResource.runInAsyncScope`][], and other transitions. Receives the newly +set internal `AsyncContextFrame` instance. It is not emitted when +[`--no-async-context-frame`][] is used. + #### Console > Stability: 1 - Experimental @@ -1413,6 +1427,9 @@ Emitted when a new thread is created. [TracingChannel Channels]: #tracingchannel-channels [`'uncaughtException'`]: process.md#event-uncaughtexception +[`--no-async-context-frame`]: cli.md#--no-async-context-frame +[`AsyncLocalStorage.enterWith`]: async_context.md#asynclocalstorageenterwithstore +[`AsyncResource.runInAsyncScope`]: async_context.md#asyncresourceruninasyncscopefn-thisarg-args [`TracingChannel`]: #class-tracingchannel [`Worker`]: worker_threads.md#class-worker [`asyncEnd` event]: #asyncendevent diff --git a/lib/internal/async_context_frame.js b/lib/internal/async_context_frame.js index cac193e3318805..c805ad7bd06f01 100644 --- a/lib/internal/async_context_frame.js +++ b/lib/internal/async_context_frame.js @@ -10,6 +10,9 @@ const { setContinuationPreservedEmbedderData, } = internalBinding('async_context_frame'); +const { channel } = require('diagnostics_channel'); +const onSet = channel('async_context_frame.set'); + let enabled_; class ActiveAsyncContextFrame extends SafeMap { @@ -23,6 +26,7 @@ class ActiveAsyncContextFrame extends SafeMap { static set(frame) { setContinuationPreservedEmbedderData(frame); + onSet.publish(frame); } static exchange(frame) { diff --git a/test/async-hooks/test-async-local-storage-set-dc.js b/test/async-hooks/test-async-local-storage-set-dc.js new file mode 100644 index 00000000000000..e3da88a3d5c3f0 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-set-dc.js @@ -0,0 +1,58 @@ +'use strict'; +require('../common'); + +if (process.execArgv.includes('--no-async-context-frame')) { + // Skipping test as it is only relevant when async_context_frame is not disabled. + process.exit(0); +} + +const assert = require('assert'); +const { subscribe } = require('diagnostics_channel'); +const { AsyncLocalStorage, AsyncResource } = require('async_hooks'); + +let frameSetCounter = 0; +let lastPublishedFrame = 0; +subscribe('async_context_frame.set', (frame) => { + frameSetCounter++; + lastPublishedFrame = frame; +}); + +const asyncLocalStorage = new AsyncLocalStorage(); +assert.strictEqual(frameSetCounter, 0); +let lastObservedSetCounter = 0; + +function assertFrameWasSet(expectedStore) { + assert(frameSetCounter > lastObservedSetCounter); + assert.strictEqual(lastPublishedFrame?.get(asyncLocalStorage), expectedStore); + lastObservedSetCounter = frameSetCounter; +} + +setImmediate(() => { + // Entering an immediate callback sets the frame. + assertFrameWasSet(undefined); + const store = { foo: 'bar' }; + asyncLocalStorage.enterWith(store); + // enterWith sets the frame. + assertFrameWasSet(store); + assert.strictEqual(asyncLocalStorage.getStore(), store); + + setTimeout(() => { + // Entering a timeout callback sets the frame. + assertFrameWasSet(store); + assert.strictEqual(asyncLocalStorage.getStore(), store); + const res = new AsyncResource('test'); + const store2 = { foo: 'bar2' }; + asyncLocalStorage.enterWith(store2); + // enterWith sets the frame. + assertFrameWasSet(store2); + res.runInAsyncScope(() => { + // runInAsyncScope sets the frame on entry. + assertFrameWasSet(store); + // AsyncResource was constructed before store2 assignment, so it should + // keep reflecting the old store. + assert.strictEqual(asyncLocalStorage.getStore(), store); + }); + // runInAsyncScope sets the frame on exit. + assertFrameWasSet(store2); + }, 10); +});