From 9128df4af81240b8cc740e6508afbfa0b5a4f2ae Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Sun, 9 Oct 2022 16:20:05 -0700 Subject: [PATCH] lib: add tracing channel to diagnostics_channel --- doc/api/diagnostics_channel.md | 472 ++++++++++++++++++ lib/diagnostics_channel.js | 269 +++++++++- .../test-diagnostics-channel-bind-store.js | 87 ++++ ...ics-channel-tracing-channel-async-error.js | 46 ++ ...agnostics-channel-tracing-channel-async.js | 51 ++ ...tics-channel-tracing-channel-sync-error.js | 39 ++ ...iagnostics-channel-tracing-channel-sync.js | 38 ++ tools/doc/type-parser.mjs | 3 + 8 files changed, 984 insertions(+), 21 deletions(-) create mode 100644 test/parallel/test-diagnostics-channel-bind-store.js create mode 100644 test/parallel/test-diagnostics-channel-tracing-channel-async-error.js create mode 100644 test/parallel/test-diagnostics-channel-tracing-channel-async.js create mode 100644 test/parallel/test-diagnostics-channel-tracing-channel-sync-error.js create mode 100644 test/parallel/test-diagnostics-channel-tracing-channel-sync.js diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index b862c9203ee9e1..76b132727c06b4 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -227,6 +227,60 @@ diagnostics_channel.subscribe('my-channel', onMessage); diagnostics_channel.unsubscribe('my-channel', onMessage); ``` +#### `diagnostics_channel.tracingChannel(nameOrChannels)` + + + +* `nameOrChannels` {string|TracingChannel} Channel name or channel collection +* Returns: {TracingChannel} Collection of channels to trace with + +Creates a [`TracingChannel`][] wrapper for `start`, `end`, `asyncStart`, +`asyncEnd`, and `error` channels within the given namespace prefixed by +`tracing:`. If a name is given, four channels will be created in the form of: + +* `tracing:${name}:start` tracks sync start of a task +* `tracing:${name}:end` tracks sync end of a task +* `tracing:${name}:error` tracks any errors either sync or async +* `tracing:${name}:asyncStart` tracks task callback or promise continuation + being reached +* `tracing:${name}:asyncEnd` tracks task callback or promise continuation + ending, promises will emit asyncEnd immediately after asyncStart + +```mjs +import diagnostics_channel from 'node:diagnostics_channel'; + +const channelsByName = diagnostics_channel.tracingChannel('my-channel'); + +// or... + +const channelsByCollection = diagnostics_channel.tracingChannel({ + start: diagnostics_channel.channel('tracing:my-channel:start'), + end: diagnostics_channel.channel('tracing:my-channel:end'), + asyncStart: diagnostics_channel.channel('tracing:my-channel:asyncStart'), + asyncEnd: diagnostics_channel.channel('tracing:my-channel:asyncEnd'), + error: diagnostics_channel.channel('tracing:my-channel:error'), +}); +``` + +```cjs +const diagnostics_channel = require('node:diagnostics_channel'); + +const channelsByName = diagnostics_channel.tracingChannel('my-channel'); + +// or... + +const channelsByCollection = diagnostics_channel.tracingChannel({ + start: diagnostics_channel.channel('tracing:my-channel:start'), + end: diagnostics_channel.channel('tracing:my-channel:end'), + asyncStart: diagnostics_channel.channel('tracing:my-channel:asyncStart'), + asyncEnd: diagnostics_channel.channel('tracing:my-channel:asyncEnd'), + error: diagnostics_channel.channel('tracing:my-channel:error'), +}); +``` + ### Class: `Channel` + +* `store` {AsyncLocalStorage} The store to which to bind the event data +* `transform` {Function} Transform the event data before setting the context + +When [`channel.runStores(data, ...)`][] is called, the given event data will +be applied to any store bound to the channel. If the store has already been +bound the previous `transform` function will be replaced with the new one. +The `transform` function may be omitted to have the event data be used as the +context directly. + +```mjs +import diagnostics_channel from 'node:diagnostics_channel'; +import { AsyncLocalStorage } from 'node:async_hooks'; + +const store = new AsyncLocalStorage(); + +const channel = diagnostics_channel.channel('my-channel'); + +channel.bindStore(store, (message) => { + return { message }; +}); +``` + +```cjs +const diagnostics_channel = require('node:diagnostics_channel'); +const { AsyncLocalStorage } = require('node:async_hooks'); + +const store = new AsyncLocalStorage(); + +const channel = diagnostics_channel.channel('my-channel'); + +channel.bindStore(store, (message) => { + return { message }; +}); +``` + +#### `channel.unbindStore(store)` + + + +* `store` {AsyncLocalStorage} The store to unbind from the channel +* Returns: {boolean} `true` if the store was found, `false` otherwise. + +Remove a message handler previously registered to this channel with +[`channel.bindStore(store)`][]. + +```mjs +import diagnostics_channel from 'node:diagnostics_channel'; +import { AsyncLocalStorage } from 'node:async_hooks'; + +const store = new AsyncLocalStorage(); + +const channel = diagnostics_channel.channel('my-channel'); + +channel.bindStore(store); +channel.unbindStore(store); +``` + +```cjs +const diagnostics_channel = require('node:diagnostics_channel'); +const { AsyncLocalStorage } = require('node:async_hooks'); + +const store = new AsyncLocalStorage(); + +const channel = diagnostics_channel.channel('my-channel'); + +channel.bindStore(store); +channel.unbindStore(store); +``` + +#### `channel.runStores(data, fn[, thisArg[, ...args]])` + + + +* `data` {any} Message to send to subscribers and bind to stores +* `fn` {Function} Handler to run within the entered storage context +* `thisArg` {any} The receiver to be used for the function call. +* `...args` {any} Optional arguments to pass to the function. + +Publishes data to the channel and applies it to any AsyncLocalStorage instances +bound to the channel for the duration of the given function. If a transform +function was given to [`channel.bindStore(store)`][] it will be applied to +transform the message data before it becomes to context value for the store. + +```mjs +import diagnostics_channel from 'node:diagnostics_channel'; +import { AsyncLocalStorage } from 'node:async_hooks'; + +const store = new AsyncLocalStorage(); + +const channel = diagnostics_channel.channel('my-channel'); + +channel.bindStore(store, (message) => { + const parent = store.getStore(); + return new Span(message, parent); +}); +channel.runStores({ some: 'message' }, () => { + store.getStore(); // Span({ some: 'message' }) +}); +``` + +```cjs +const diagnostics_channel = require('node:diagnostics_channel'); +const { AsyncLocalStorage } = require('node:async_hooks'); + +const store = new AsyncLocalStorage(); + +const channel = diagnostics_channel.channel('my-channel'); + +channel.bindStore(store, (message) => { + const parent = store.getStore(); + return new Span(message, parent); +}); +channel.runStores({ some: 'message' }, () => { + store.getStore(); // Span({ some: 'message' }) +}); +``` + +### Class: `TracingChannel` + + + +The class `TracingChannel` represents a collection of named channels which +together express a trace. It is used to formalize and simplify the process of +producing tracing events. [`diagnostics_channel.tracingChannel()`][] is used +to construct a `TracingChannel`. As with `Channel` it is recommended to create +and reuse a single `TracingChannel` at the top-level of the file rather than +creating them dynamically. + +Tracing channels should follow a naming pattern of: + +* `tracing:module.class.method:start` or `tracing:module.function:start` +* `tracing:module.class.method:end` or `tracing:module.function:end` +* `tracing:module.class.method:asyncStart` or `tracing:module.function:asyncStart` +* `tracing:module.class.method:asyncEnd` or `tracing:module.function:asyncEnd` +* `tracing:module.class.method:error` or `tracing:module.function:error` + +#### `tracingChannel.subscribe(handlers)` + + + +* `handlers` {Object} Set of functions to subscribe to corresponding channels + * `start` {Function} Handler to subscribe to `start` channel + * `end` {Function} Handler to subscribe to `end` channel + * `asyncStart` {Function} Handler to subscribe to `asyncStart` channel + * `asyncEnd` {Function} Handler to subscribe to `asyncEnd` channel + * `error` {Function} Handler to subscribe to `error` channel + +Helper to subscribe a collection of functions to the corresponding channels. +This is the same as calling [`channel.subscribe(onMessage)`][] on each channel +individually. + +```mjs +import diagnostics_channel from 'node:diagnostics_channel'; + +const channels = diagnostics_channel.tracingChannel('my-channel'); + +channels.subscribe({ + start(message) { + // Handle start message + }, + end(message) { + // Handle end message + }, + asyncStart(message) { + // Handle asyncEnd message + }, + asyncEnd(message) { + // Handle asyncEnd message + }, + error(message) { + // Handle error message + }, +}); +``` + +```cjs +const diagnostics_channel = require('node:diagnostics_channel'); + +const channels = diagnostics_channel.tracingChannel('my-channel'); + +channels.subscribe({ + start(message) { + // Handle start message + }, + end(message) { + // Handle end message + }, + asyncStart(message) { + // Handle asyncEnd message + }, + asyncEnd(message) { + // Handle asyncEnd message + }, + error(message) { + // Handle error message + }, +}); +``` + +#### `tracingChannel.unsubscribe(handlers)` + + + +* `handlers` {Object} Set of functions to unsubscribe from channels + * `start` {Function} Handler to unsubscribe from `start` channel + * `end` {Function} Handler to unsubscribe from `end` channel + * `asyncStart` {Function} Handler to unsubscribe from `asyncStart` channel + * `asyncEnd` {Function} Handler to unsubscribe from `asyncEnd` channel + * `error` {Function} Handler to unsubscribe from `error` channel +* Returns: {boolean} If all handlers were successfully unsubscribed + +Helper to unsubscribe a collection of functions from the corresponding channels. +This is the same as calling [`channel.unsubscribe(onMessage)`][] on each channel +individually. + +```mjs +import diagnostics_channel from 'node:diagnostics_channel'; + +const channels = diagnostics_channel.tracingChannel('my-channel'); + +channels.unsubscribe({ + start(message) { + // Handle start message + }, + end(message) { + // Handle end message + }, + asyncStart(message) { + // Handle asyncEnd message + }, + asyncEnd(message) { + // Handle asyncEnd message + }, + error(message) { + // Handle error message + }, +}); +``` + +```cjs +const diagnostics_channel = require('node:diagnostics_channel'); + +const channels = diagnostics_channel.tracingChannel('my-channel'); + +channels.unsubscribe({ + start(message) { + // Handle start message + }, + end(message) { + // Handle end message + }, + asyncStart(message) { + // Handle asyncEnd message + }, + asyncEnd(message) { + // Handle asyncEnd message + }, + error(message) { + // Handle error message + }, +}); +``` + +#### `tracingChannel.traceSync(fn[, ctx[, thisArg, [, ...args]]])` + + + +* `fn` {Function} Function to wrap a trace around +* `ctx` {Object} Shared context object to correlate trace events through +* `thisArg` {any} The receiver to be used for the function call +* `...args` {any} Optional arguments to pass to the function +* Returns: {any} The return value of the given function + +Trace a sync function call. This will always produce `start` and `end` events +around the execution and may also produce an `error` event if the given +function throws an error. + +```mjs +import diagnostics_channel from 'node:diagnostics_channel'; + +const channels = diagnostics_channel.tracingChannel('my-channel'); + +channels.traceSync(() => { + // Do something +}, { + context: 'something', +}); +``` + +```cjs +const diagnostics_channel = require('node:diagnostics_channel'); + +const channels = diagnostics_channel.tracingChannel('my-channel'); + +channels.traceSync(() => { + // Do something +}, { + context: 'something', +}); +``` + +#### `tracingChannel.tracePromise(fn[, ctx[, thisArg, [, ...args]]])` + + + +* `fn` {Function} Promise-returning function to wrap a trace around +* `ctx` {Object} Shared context object to correlate trace events through +* `thisArg` {any} The receiver to be used for the function call +* `...args` {any} Optional arguments to pass to the function +* Returns: {Promise} Promise returned by the given function + +Trace a promise-returning function call. This will always produce `start`, +`end`, `asyncStart`, and `asyncEnd` events around the execution and may also +produce an `error` event if the given function throws an error or the returned +promise rejects. + +```mjs +import diagnostics_channel from 'node:diagnostics_channel'; + +const channels = diagnostics_channel.tracingChannel('my-channel'); + +channels.tracePromise(async () => { + // Do something +}, { + context: 'something', +}); +``` + +```cjs +const diagnostics_channel = require('node:diagnostics_channel'); + +const channels = diagnostics_channel.tracingChannel('my-channel'); + +channels.tracePromise(async () => { + // Do something +}, { + context: 'something', +}); +``` + +#### `tracingChannel.traceCallback(fn[, position[, ctx[, thisArg, [, ...args]]]])` + + + +* `fn` {Function} Promise-returning function to wrap a trace around +* `position` {number} Zero-indexed argument position of expected callback +* `ctx` {Object} Shared context object to correlate trace events through +* `thisArg` {any} The receiver to be used for the function call +* `...args` {any} Optional arguments to pass to the function +* Returns: {Promise} Promise returned by the given function + +Trace a promise-returning function call. This will always produce `start`, +`end`, `asyncStart`, and `asyncEnd` events around the execution and may also +produce an `error` event if the given function throws an error or the returned +promise rejects. + +```mjs +import diagnostics_channel from 'node:diagnostics_channel'; + +const channels = diagnostics_channel.tracingChannel('my-channel'); + +channels.traceCallback((arg1, callback) => { + // Do something + callback(null, 'result'); +}, { + context: 'something', +}, thisArg, arg1, callback); +``` + +```cjs +const diagnostics_channel = require('node:diagnostics_channel'); + +const channels = diagnostics_channel.tracingChannel('my-channel'); + +channels.traceCallback((arg1, callback) => { + // Do something + callback(null, 'result'); +}, { + context: 'something', +}, thisArg, arg1, callback); +``` + ### Built-in Channels > Stability: 1 - Experimental @@ -495,8 +962,13 @@ added: v16.18.0 Emitted when a new thread is created. [`'uncaughtException'`]: process.md#event-uncaughtexception +[`TracingChannel`]: #class-tracingchannel [`Worker`]: worker_threads.md#class-worker +[`channel.bindStore(store)`]: #channelbindstorestore-transform +[`channel.runStores(data, ...)`]: #channelrunstoresdata-fn-thisarg-args [`channel.subscribe(onMessage)`]: #channelsubscribeonmessage +[`channel.unsubscribe(onMessage)`]: #channelunsubscribeonmessage [`diagnostics_channel.channel(name)`]: #diagnostics_channelchannelname [`diagnostics_channel.subscribe(name, onMessage)`]: #diagnostics_channelsubscribename-onmessage +[`diagnostics_channel.tracingChannel()`]: #diagnostics_channeltracingchannelnameorchannels [`diagnostics_channel.unsubscribe(name, onMessage)`]: #diagnostics_channelunsubscribename-onmessage diff --git a/lib/diagnostics_channel.js b/lib/diagnostics_channel.js index 9d2d805bf25052..b42416b7c00e0a 100644 --- a/lib/diagnostics_channel.js +++ b/lib/diagnostics_channel.js @@ -4,9 +4,14 @@ const { ArrayPrototypeIndexOf, ArrayPrototypePush, ArrayPrototypeSplice, + FunctionPrototypeBind, ObjectCreate, ObjectGetPrototypeOf, ObjectSetPrototypeOf, + PromisePrototypeThen, + PromiseReject, + ReflectApply, + SafeMap, SymbolHasInstance, } = primordials; @@ -23,11 +28,40 @@ const { triggerUncaughtException } = internalBinding('errors'); const { WeakReference } = internalBinding('util'); +function decRef(channel) { + channel._weak.decRef(); + if (channel._weak.getRef() === 0) { + delete channels[channel.name]; + } +} + +function markActive(channel) { + // eslint-disable-next-line no-use-before-define + ObjectSetPrototypeOf(channel, ActiveChannel.prototype); + channel._subscribers = []; + channel._stores = new SafeMap(); +} + +function maybeMarkInactive(channel) { + // When there are no more active subscribers, restore to fast prototype. + if (!channel._subscribers.length && !channel._stores.size) { + // eslint-disable-next-line no-use-before-define + ObjectSetPrototypeOf(channel, Channel.prototype); + channel._subscribers = undefined; + channel._stores = undefined; + } +} + +function wrapStoreRun(store, data, next, transform = (v) => v) { + return () => store.run(transform(data), next); +} + // TODO(qard): should there be a C++ channel interface? class ActiveChannel { subscribe(subscription) { validateFunction(subscription, 'subscription'); ArrayPrototypePush(this._subscribers, subscription); + this._weak.incRef(); } unsubscribe(subscription) { @@ -36,12 +70,28 @@ class ActiveChannel { ArrayPrototypeSplice(this._subscribers, index, 1); - // When there are no more active subscribers, restore to fast prototype. - if (!this._subscribers.length) { - // eslint-disable-next-line no-use-before-define - ObjectSetPrototypeOf(this, Channel.prototype); + decRef(this); + maybeMarkInactive(this); + + return true; + } + + bindStore(store, transform) { + const replacing = this._stores.has(store); + if (!replacing) this._weak.incRef(); + this._stores.set(store, transform); + } + + unbindStore(store) { + if (!this._stores.has(store)) { + return false; } + this._stores.delete(store); + + decRef(this); + maybeMarkInactive(this); + return true; } @@ -61,11 +111,28 @@ class ActiveChannel { } } } + + runStores(data, fn, thisArg, ...args) { + this.publish(data); + + // Bind base fn first due to AsyncLocalStorage.run not having thisArg + fn = FunctionPrototypeBind(fn, thisArg, ...args); + + for (const entry of this._stores.entries()) { + const store = entry[0]; + const transform = entry[1]; + fn = wrapStoreRun(store, data, fn, transform); + } + + return fn(); + } } class Channel { constructor(name) { this._subscribers = undefined; + this._stores = undefined; + this._weak = undefined; this.name = name; } @@ -76,8 +143,7 @@ class Channel { } subscribe(subscription) { - ObjectSetPrototypeOf(this, ActiveChannel.prototype); - this._subscribers = []; + markActive(this); this.subscribe(subscription); } @@ -85,11 +151,24 @@ class Channel { return false; } + bindStore(store, transform = (v) => v) { + markActive(this); + this.bindStore(store, transform); + } + + unbindStore() { + return false; + } + get hasSubscribers() { return false; } publish() {} + + runStores(data, fn, thisArg, ...args) { + return ReflectApply(fn, thisArg, args); + } } const channels = ObjectCreate(null); @@ -105,27 +184,17 @@ function channel(name) { } channel = new Channel(name); - channels[name] = new WeakReference(channel); + channel._weak = new WeakReference(channel); + channels[name] = channel._weak; return channel; } function subscribe(name, subscription) { - const chan = channel(name); - channels[name].incRef(); - chan.subscribe(subscription); + return channel(name).subscribe(subscription); } function unsubscribe(name, subscription) { - const chan = channel(name); - if (!chan.unsubscribe(subscription)) { - return false; - } - - channels[name].decRef(); - if (channels[name].getRef() === 0) { - delete channels[name]; - } - return true; + return channel(name).unsubscribe(subscription); } function hasSubscribers(name) { @@ -139,10 +208,168 @@ function hasSubscribers(name) { return channel.hasSubscribers; } +const traceEvents = [ + 'start', + 'end', + 'asyncStart', + 'asyncEnd', + 'error', +]; + +function assertChannel(value, name) { + if (!(value instanceof Channel)) { + throw new ERR_INVALID_ARG_TYPE(name, ['Channel'], value); + } +} + +class TracingChannel { + constructor(nameOrChannels) { + if (typeof nameOrChannels === 'string') { + this.start = channel(`tracing:${nameOrChannels}:start`); + this.end = channel(`tracing:${nameOrChannels}:end`); + this.asyncStart = channel(`tracing:${nameOrChannels}:asyncStart`); + this.asyncEnd = channel(`tracing:${nameOrChannels}:asyncEnd`); + this.error = channel(`tracing:${nameOrChannels}:error`); + } else if (typeof nameOrChannels === 'object') { + const { start, end, asyncStart, asyncEnd, error } = nameOrChannels; + + assertChannel(start, 'nameOrChannels.start'); + assertChannel(end, 'nameOrChannels.end'); + assertChannel(asyncStart, 'nameOrChannels.asyncStart'); + assertChannel(asyncEnd, 'nameOrChannels.asyncEnd'); + assertChannel(error, 'nameOrChannels.error'); + + this.start = start; + this.end = end; + this.asyncStart = asyncStart; + this.asyncEnd = asyncEnd; + this.error = error; + } else { + throw new ERR_INVALID_ARG_TYPE('nameOrChannels', + ['string', 'object', 'Channel'], + nameOrChannels); + } + } + + subscribe(handlers) { + for (const name of traceEvents) { + if (!handlers[name]) continue; + + this[name]?.subscribe(handlers[name]); + } + } + + unsubscribe(handlers) { + let done = true; + + for (const name of traceEvents) { + if (!handlers[name]) continue; + + if (!this[name]?.unsubscribe(handlers[name])) { + done = false; + } + } + + return done; + } + + traceSync(fn, ctx = {}, thisArg, ...args) { + const { start, end, error } = this; + + try { + const result = start.runStores(ctx, fn, thisArg, ...args); + ctx.result = result; + return result; + } catch (err) { + ctx.error = err; + error.publish(ctx); + throw err; + } finally { + end.publish(ctx); + } + } + + tracePromise(fn, ctx = {}, thisArg, ...args) { + const { start, end, asyncStart, asyncEnd, error } = this; + + function reject(err) { + ctx.error = err; + error.publish(ctx); + asyncStart.publish(ctx); + // TODO: Is there a way to have asyncEnd _after_ the continuation? + asyncEnd.publish(ctx); + return PromiseReject(err); + } + + function resolve(result) { + ctx.result = result; + asyncStart.publish(ctx); + // TODO: Is there a way to have asyncEnd _after_ the continuation? + asyncEnd.publish(ctx); + return result; + } + + try { + const promise = start.runStores(ctx, fn, thisArg, ...args); + return PromisePrototypeThen(promise, resolve, reject); + } catch (err) { + ctx.error = err; + error.publish(ctx); + throw err; + } finally { + end.publish(ctx); + } + } + + traceCallback(fn, position = 0, ctx = {}, thisArg, ...args) { + const { start, end, asyncStart, asyncEnd, error } = this; + + function wrap(fn) { + return function wrappedCallback(err, res) { + if (err) { + ctx.error = err; + error.publish(ctx); + } else { + ctx.result = res; + } + + asyncStart.publish(ctx); + try { + if (fn) { + return ReflectApply(fn, this, arguments); + } + } finally { + asyncEnd.publish(ctx); + } + }; + } + + if (position >= 0) { + args.splice(position, 1, wrap(args.at(position))); + } + + try { + return start.runStores(ctx, fn, thisArg, ...args); + } catch (err) { + ctx.error = err; + error.publish(ctx); + throw err; + } finally { + end.publish(ctx); + } + } +} + +function tracingChannel(nameOrChannels) { + return new TracingChannel(nameOrChannels); +} + module.exports = { channel, hasSubscribers, subscribe, + tracingChannel, unsubscribe, - Channel + Channel, + TracingChannel }; diff --git a/test/parallel/test-diagnostics-channel-bind-store.js b/test/parallel/test-diagnostics-channel-bind-store.js new file mode 100644 index 00000000000000..076cf4350f9be6 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-bind-store.js @@ -0,0 +1,87 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dc = require('diagnostics_channel'); +const { AsyncLocalStorage } = require('async_hooks'); + +let n = 0; +const thisArg = new Date(); +const inputs = [ + { foo: 'bar' }, + { baz: 'buz' }, +]; + +const channel = dc.channel('test'); + +// Bind a storage directly to published data +const store1 = new AsyncLocalStorage(); +channel.bindStore(store1); + +// Bind a store with transformation of published data +const store2 = new AsyncLocalStorage(); +channel.bindStore(store2, common.mustCall((data) => { + assert.deepStrictEqual(data, inputs[n]); + return { data }; +}, 3)); + +// Regular subscribers should see publishes from runStores calls +channel.subscribe(common.mustCall((data) => { + assert.deepStrictEqual(data, inputs[n]); +}, 3)); + +// Verify stores are empty before run +assert.strictEqual(store1.getStore(), undefined); +assert.strictEqual(store2.getStore(), undefined); + +channel.runStores(inputs[n], common.mustCall(function(a, b) { + // Verify this and argument forwarding + assert.deepStrictEqual(this, thisArg); + assert.strictEqual(a, 1); + assert.strictEqual(b, 2); + + // Verify store 1 state matches input + assert.deepStrictEqual(store1.getStore(), inputs[n]); + + // Verify store 2 state has expected transformation + assert.deepStrictEqual(store2.getStore(), { data: inputs[n] }); + + // Should support nested contexts + n++; + channel.runStores(inputs[n], common.mustCall(function() { + // Verify this and argument forwarding + assert.strictEqual(this, undefined); + + // Verify store 1 state matches input + assert.deepStrictEqual(store1.getStore(), inputs[n]); + + // Verify store 2 state has expected transformation + assert.deepStrictEqual(store2.getStore(), { data: inputs[n] }); + })); + n--; + + // Verify store 1 state matches input + assert.deepStrictEqual(store1.getStore(), inputs[n]); + + // Verify store 2 state has expected transformation + assert.deepStrictEqual(store2.getStore(), { data: inputs[n] }); +}), thisArg, 1, 2); + +// Verify stores are empty after run +assert.strictEqual(store1.getStore(), undefined); +assert.strictEqual(store2.getStore(), undefined); + +// Verify unbinding works +assert.ok(channel.unbindStore(store1)); + +// Verify unbinding a store that is not bound returns false +assert.ok(!channel.unbindStore(store1)); + +n++; +channel.runStores(inputs[n], common.mustCall(() => { + // Verify after unbinding store 1 will remain undefined + assert.strictEqual(store1.getStore(), undefined); + + // Verify still bound store 2 receives expected data + assert.deepStrictEqual(store2.getStore(), { data: inputs[n] }); +})); diff --git a/test/parallel/test-diagnostics-channel-tracing-channel-async-error.js b/test/parallel/test-diagnostics-channel-tracing-channel-async-error.js new file mode 100644 index 00000000000000..7335e15de9dfb5 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-tracing-channel-async-error.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); + +const expectedError = new Error('test'); +const input = { foo: 'bar' }; +const thisArg = { baz: 'buz' }; + +function check(found) { + assert.deepStrictEqual(found, input); +} + +const handlers = { + start: common.mustCall(check, 2), + end: common.mustCall(check, 2), + asyncStart: common.mustCall(check, 2), + asyncEnd: common.mustCall(check, 2), + error: common.mustCall((found) => { + check(found); + assert.deepStrictEqual(found.error, expectedError); + }, 2) +}; + +channel.subscribe(handlers); + +channel.traceCallback(function(cb, err) { + assert.deepStrictEqual(this, thisArg); + setImmediate(cb, err); +}, 0, input, thisArg, common.mustCall((err, res) => { + assert.strictEqual(err, expectedError); + assert.strictEqual(res, undefined); +}), expectedError); + +channel.tracePromise(function(value) { + assert.deepStrictEqual(this, thisArg); + return Promise.reject(value); +}, input, thisArg, expectedError).then( + common.mustNotCall(), + common.mustCall((value) => { + assert.deepStrictEqual(value, expectedError); + }) +); diff --git a/test/parallel/test-diagnostics-channel-tracing-channel-async.js b/test/parallel/test-diagnostics-channel-tracing-channel-async.js new file mode 100644 index 00000000000000..57c6c6a647e990 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-tracing-channel-async.js @@ -0,0 +1,51 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); + +const expectedResult = { foo: 'bar' }; +const input = { foo: 'bar' }; +const thisArg = { baz: 'buz' }; + +function check(found) { + assert.deepStrictEqual(found, input); +} + +const handlers = { + start: common.mustCall(check, 2), + end: common.mustCall(check, 2), + asyncStart: common.mustCall((found) => { + check(found); + assert.strictEqual(found.error, undefined); + assert.deepStrictEqual(found.result, expectedResult); + }, 2), + asyncEnd: common.mustCall((found) => { + check(found); + assert.strictEqual(found.error, undefined); + assert.deepStrictEqual(found.result, expectedResult); + }, 2), + error: common.mustNotCall() +}; + +channel.subscribe(handlers); + +channel.traceCallback(function(cb, err, res) { + assert.deepStrictEqual(this, thisArg); + setImmediate(cb, err, res); +}, 0, input, thisArg, common.mustCall((err, res) => { + assert.strictEqual(err, null); + assert.deepStrictEqual(res, expectedResult); +}), null, expectedResult); + +channel.tracePromise(function(value) { + assert.deepStrictEqual(this, thisArg); + return Promise.resolve(value); +}, input, thisArg, expectedResult).then( + common.mustCall((value) => { + assert.deepStrictEqual(value, expectedResult); + }), + common.mustNotCall() +); diff --git a/test/parallel/test-diagnostics-channel-tracing-channel-sync-error.js b/test/parallel/test-diagnostics-channel-tracing-channel-sync-error.js new file mode 100644 index 00000000000000..0965bf3fb495f0 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-tracing-channel-sync-error.js @@ -0,0 +1,39 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); + +const expectedError = new Error('test'); +const input = { foo: 'bar' }; +const thisArg = { baz: 'buz' }; + +function check(found) { + assert.deepStrictEqual(found, input); +} + +const handlers = { + start: common.mustCall(check), + end: common.mustCall(check), + asyncStart: common.mustNotCall(), + asyncEnd: common.mustNotCall(), + error: common.mustCall((found) => { + check(found); + assert.deepStrictEqual(found.error, expectedError); + }) +}; + +channel.subscribe(handlers); +try { + channel.traceSync(function(err) { + assert.deepStrictEqual(this, thisArg); + assert.strictEqual(err, expectedError); + throw err; + }, input, thisArg, expectedError); + + throw new Error('It should not reach this error'); +} catch (error) { + assert.deepStrictEqual(error, expectedError); +} diff --git a/test/parallel/test-diagnostics-channel-tracing-channel-sync.js b/test/parallel/test-diagnostics-channel-tracing-channel-sync.js new file mode 100644 index 00000000000000..89f28916006971 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-tracing-channel-sync.js @@ -0,0 +1,38 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); + +const expectedResult = { foo: 'bar' }; +const input = { foo: 'bar' }; + +function check(found) { + assert.deepStrictEqual(found, input); +} + +const handlers = { + start: common.mustCall(check), + end: common.mustCall((found) => { + check(found); + assert.deepStrictEqual(found.result, expectedResult); + }), + asyncStart: common.mustNotCall(), + asyncEnd: common.mustNotCall(), + error: common.mustNotCall() +}; + +assert.strictEqual(channel.start.hasSubscribers, false); +channel.subscribe(handlers); +assert.strictEqual(channel.start.hasSubscribers, true); +channel.traceSync(() => { + return expectedResult; +}, input); + +channel.unsubscribe(handlers); +assert.strictEqual(channel.start.hasSubscribers, false); +channel.traceSync(() => { + return expectedResult; +}, input); diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index 64d499d182f484..016b99d9d3afc6 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -57,6 +57,8 @@ const customTypesMap = { 'Module Namespace Object': 'https://tc39.github.io/ecma262/#sec-module-namespace-exotic-objects', + 'AsyncLocalStorage': 'async_context.html#class-asynclocalstorage', + 'AsyncHook': 'async_hooks.html#async_hookscreatehookcallbacks', 'AsyncResource': 'async_hooks.html#class-asyncresource', @@ -108,6 +110,7 @@ const customTypesMap = { 'dgram.Socket': 'dgram.html#class-dgramsocket', 'Channel': 'diagnostics_channel.html#class-channel', + 'TracingChannel': 'diagnostics_channel.html#class-tracingchannel', 'Domain': 'domain.html#class-domain',