Skip to content

Commit

Permalink
diagnostics_channel: early-exit tracing channel trace methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Stephen Belanger committed Feb 28, 2024
1 parent f4af4b1 commit 8edb24d
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 100 deletions.
20 changes: 20 additions & 0 deletions doc/api/diagnostics_channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,11 @@ if the given function throws an error. This will run the given function using
[`channel.runStores(context, ...)`][] on the `start` channel which ensures all
events should have any bound stores set to match this trace context.

To ensure only correct trace graphs are formed, events will only be published
if subscribers are present prior to starting the trace. Subscriptions which are
added after the trace begins will not receive future events from that trace,
only future traces will be seen.

```mjs
import diagnostics_channel from 'node:diagnostics_channel';

Expand Down Expand Up @@ -838,6 +843,11 @@ returned promise rejects. This will run the given function using
[`channel.runStores(context, ...)`][] on the `start` channel which ensures all
events should have any bound stores set to match this trace context.

To ensure only correct trace graphs are formed, events will only be published
if subscribers are present prior to starting the trace. Subscriptions which are
added after the trace begins will not receive future events from that trace,
only future traces will be seen.

```mjs
import diagnostics_channel from 'node:diagnostics_channel';

Expand Down Expand Up @@ -891,6 +901,11 @@ the callback is set. This will run the given function using
[`channel.runStores(context, ...)`][] on the `start` channel which ensures all
events should have any bound stores set to match this trace context.

To ensure only correct trace graphs are formed, events will only be published
if subscribers are present prior to starting the trace. Subscriptions which are
added after the trace begins will not receive future events from that trace,
only future traces will be seen.

```mjs
import diagnostics_channel from 'node:diagnostics_channel';

Expand Down Expand Up @@ -979,6 +994,11 @@ of the callback while the `error` will either be a thrown error visible in the
`end` event or the first callback argument in either of the `asyncStart` or
`asyncEnd` events.

To ensure only correct trace graphs are formed, events should only be published
if subscribers are present prior to starting the trace. Subscriptions which are
added after the trace begins should not receive future events from that trace,
only future traces will be seen.

Tracing channels should follow a naming pattern of:

* `tracing:module.class.method:start` or `tracing:module.function:start`
Expand Down
67 changes: 43 additions & 24 deletions lib/diagnostics_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
ArrayPrototypePush,
ArrayPrototypeSplice,
SafeFinalizationRegistry,
ObjectDefineProperty,
ObjectGetPrototypeOf,
ObjectSetPrototypeOf,
Promise,
Expand Down Expand Up @@ -250,35 +251,41 @@ function assertChannel(value, name) {
}
}

function channelOrName(nameOrChannels, name) {
if (typeof nameOrChannels === 'string') {
return channel(`tracing:${nameOrChannels}:${name}`)

Check failure on line 256 in lib/diagnostics_channel.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing semicolon
}

if (typeof nameOrChannels === 'object') {
const channel = nameOrChannels[name];
assertChannel(channel, `nameOrChannels.${name}`);
return channel;
}

throw new ERR_INVALID_ARG_TYPE('nameOrChannels',
['string', 'object', 'TracingChannel'],
nameOrChannels);
}

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);
for (const eventName of traceEvents) {
ObjectDefineProperty(this, eventName, {

Check failure on line 273 in lib/diagnostics_channel.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Must use null-prototype object for property descriptors
value: channelOrName(nameOrChannels, eventName)

Check failure on line 274 in lib/diagnostics_channel.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing trailing comma
});
}
}

get hasSubscribers() {
for (const name of traceEvents) {
if (this[name].hasSubscribers) {
return true

Check failure on line 282 in lib/diagnostics_channel.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing semicolon
}
}

return false

Check failure on line 286 in lib/diagnostics_channel.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing semicolon
}

subscribe(handlers) {
for (const name of traceEvents) {
if (!handlers[name]) continue;
Expand All @@ -302,6 +309,10 @@ class TracingChannel {
}

traceSync(fn, context = {}, thisArg, ...args) {
if (!this.hasSubscribers) {
return ReflectApply(fn, thisArg, args);
}

const { start, end, error } = this;

return start.runStores(context, () => {
Expand All @@ -320,6 +331,10 @@ class TracingChannel {
}

tracePromise(fn, context = {}, thisArg, ...args) {
if (!this.hasSubscribers) {
return ReflectApply(fn, thisArg, args);
}

const { start, end, asyncStart, asyncEnd, error } = this;

function reject(err) {
Expand Down Expand Up @@ -358,6 +373,10 @@ class TracingChannel {
}

traceCallback(fn, position = -1, context = {}, thisArg, ...args) {
if (!this.hasSubscribers) {
return ReflectApply(fn, thisArg, args);
}

const { start, end, asyncStart, asyncEnd, error } = this;

function wrappedCallback(err, res) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ assert.throws(() => (channel = dc.tracingChannel(0)), {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message:
/The "nameOrChannels" argument must be of type string or an instance of Channel or Object/,
/The "nameOrChannels" argument must be of type string or an instance of TracingChannel or Object/,
});

// tracingChannel creating without instance of Channel must throw error
Expand Down
60 changes: 0 additions & 60 deletions test/parallel/test-diagnostics-channel-tracing-channel-async.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';

const common = require('../common');
const dc = require('diagnostics_channel');

const channel = dc.tracingChannel('test');

const handlers = {
start: common.mustNotCall(),
end: common.mustNotCall(),
asyncStart: common.mustNotCall(),
asyncEnd: common.mustNotCall(),
error: common.mustNotCall()
};

// While subscribe occurs _before_ the callback executes,
// no async events should be published.
channel.traceCallback(setImmediate, 0, {}, null, common.mustCall());
channel.subscribe(handlers);
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ function check(found) {
}

const handlers = {
start: common.mustCall(check, 2),
end: common.mustCall(check, 2),
asyncStart: common.mustCall(check, 2),
asyncEnd: common.mustCall(check, 2),
start: common.mustCall(check),
end: common.mustCall(check),
asyncStart: common.mustCall(check),
asyncEnd: common.mustCall(check),
error: common.mustCall((found) => {
check(found);
assert.deepStrictEqual(found.error, expectedError);
}, 2)
})
};

channel.subscribe(handlers);
Expand All @@ -34,13 +34,3 @@ channel.traceCallback(function(cb, err) {
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);
})
);
43 changes: 43 additions & 0 deletions test/parallel/test-diagnostics-channel-tracing-channel-callback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'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);
}

function checkAsync(found) {
check(found);
assert.strictEqual(found.error, undefined);
assert.deepStrictEqual(found.result, expectedResult);
}

const handlers = {
start: common.mustCall(check),
end: common.mustCall(check),
asyncStart: common.mustCall(checkAsync),
asyncEnd: common.mustCall(checkAsync),
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);

assert.throws(() => {
channel.traceCallback(common.mustNotCall(), 0, input, thisArg, 1, 2, 3);
}, /"callback" argument must be of type function/);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';

const common = require('../common');
const dc = require('diagnostics_channel');

const channel = dc.tracingChannel('test');

const handlers = {
start: common.mustNotCall(),
end: common.mustNotCall(),
asyncStart: common.mustNotCall(),
asyncEnd: common.mustNotCall(),
error: common.mustNotCall()
};

// While subscribe occurs _before_ the promise resolves,
// no async events should be published.
channel.tracePromise(() => {
return new Promise(setImmediate);
}, {});
channel.subscribe(handlers);
Original file line number Diff line number Diff line change
@@ -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 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.mustCall(check),
asyncEnd: common.mustCall(check),
error: common.mustCall((found) => {
check(found);
assert.deepStrictEqual(found.error, expectedError);
})
};

channel.subscribe(handlers);

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);
})
);
Loading

0 comments on commit 8edb24d

Please sign in to comment.