Skip to content

Commit 71a5eb2

Browse files
committed
diagnostics_channel: ensure tracePromise consistency with non-Promises
1 parent 4fca20c commit 71a5eb2

File tree

4 files changed

+143
-20
lines changed

4 files changed

+143
-20
lines changed

doc/api/diagnostics_channel.md

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -829,23 +829,36 @@ channels.traceSync(() => {
829829
added:
830830
- v19.9.0
831831
- v18.19.0
832+
changes:
833+
- version: REPLACEME
834+
pr-url: https://github.com/nodejs/node/pull/61766
835+
description: Custom thenables will no longer be wrapped in native Promises.
836+
Non-thenables will be returned with a warning.
832837
-->
833838

834-
* `fn` {Function} Promise-returning function to wrap a trace around
839+
* `fn` {Function} Function to wrap a trace around
835840
* `context` {Object} Shared object to correlate trace events through
836841
* `thisArg` {any} The receiver to be used for the function call
837842
* `...args` {any} Optional arguments to pass to the function
838-
* Returns: {Promise} Chained from promise returned by the given function
839-
840-
Trace a promise-returning function call. This will always produce a
841-
[`start` event][] and [`end` event][] around the synchronous portion of the
842-
function execution, and will produce an [`asyncStart` event][] and
843-
[`asyncEnd` event][] when a promise continuation is reached. It may also
844-
produce an [`error` event][] if the given function throws an error or the
845-
returned promise rejects. This will run the given function using
843+
* Returns: {any} The return value of the given function, or the result of
844+
calling `.then(...)` on the return value if the tracing channel has active
845+
subscribers. If the return value is not a Promise or thenable, then
846+
it is returned as-is and a warning is emitted.
847+
848+
Trace an asynchronous function call which returns a {Promise} or
849+
[thenable object][]. This will always produce a [`start` event][] and
850+
[`end` event][] around the synchronous portion of the function execution, and
851+
will produce an [`asyncStart` event][] and [`asyncEnd` event][] when the
852+
returned promise is resolved or rejected. It may also produce an
853+
[`error` event][] if the given function throws an error or the returned promise
854+
is rejected. This will run the given function using
846855
[`channel.runStores(context, ...)`][] on the `start` channel which ensures all
847856
events should have any bound stores set to match this trace context.
848857

858+
If the value returned by `fn` is not a Promise or thenable, then it will be
859+
returned with a warning, and no `asyncStart` or `asyncEnd` events will be
860+
produced.
861+
849862
To ensure only correct trace graphs are formed, events will only be published
850863
if subscribers are present prior to starting the trace. Subscriptions which are
851864
added after the trace begins will not receive future events from that trace,
@@ -1457,3 +1470,4 @@ Emitted when a new thread is created.
14571470
[`process.execve()`]: process.md#processexecvefile-args-env
14581471
[`start` event]: #startevent
14591472
[context loss]: async_context.md#troubleshooting-context-loss
1473+
[thenable object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables

lib/diagnostics_channel.js

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@ const {
1010
ObjectDefineProperty,
1111
ObjectGetPrototypeOf,
1212
ObjectSetPrototypeOf,
13-
Promise,
14-
PromisePrototypeThen,
15-
PromiseReject,
16-
PromiseResolve,
1713
ReflectApply,
1814
SafeFinalizationRegistry,
1915
SafeMap,
@@ -280,6 +276,11 @@ function tracingChannelFrom(nameOrChannels, name) {
280276
nameOrChannels);
281277
}
282278

279+
function emitNonThenableWarning(fn) {
280+
process.emitWarning(`tracePromise was called with the function '${fn.name || '<anonymous>'}', ` +
281+
'which returned a non-thenable.');
282+
}
283+
283284
class TracingChannel {
284285
constructor(nameOrChannels) {
285286
for (let i = 0; i < traceEvents.length; ++i) {
@@ -347,7 +348,11 @@ class TracingChannel {
347348

348349
tracePromise(fn, context = {}, thisArg, ...args) {
349350
if (!this.hasSubscribers) {
350-
return ReflectApply(fn, thisArg, args);
351+
const result = ReflectApply(fn, thisArg, args);
352+
if (typeof result.then !== 'function') {
353+
emitNonThenableWarning(fn);
354+
}
355+
return result;
351356
}
352357

353358
const { start, end, asyncStart, asyncEnd, error } = this;
@@ -358,7 +363,7 @@ class TracingChannel {
358363
asyncStart.publish(context);
359364
// TODO: Is there a way to have asyncEnd _after_ the continuation?
360365
asyncEnd.publish(context);
361-
return PromiseReject(err);
366+
throw err;
362367
}
363368

364369
function resolve(result) {
@@ -371,12 +376,15 @@ class TracingChannel {
371376

372377
return start.runStores(context, () => {
373378
try {
374-
let promise = ReflectApply(fn, thisArg, args);
375-
// Convert thenables to native promises
376-
if (!(promise instanceof Promise)) {
377-
promise = PromiseResolve(promise);
379+
const result = ReflectApply(fn, thisArg, args);
380+
// If the return value is not a thenable, then return it with a warning.
381+
// Do not publish to asyncStart/asyncEnd.
382+
if (typeof result.then !== 'function') {
383+
emitNonThenableWarning(fn);
384+
context.result = result;
385+
return result;
378386
}
379-
return PromisePrototypeThen(promise, resolve, reject);
387+
return result.then(resolve, reject);
380388
} catch (err) {
381389
context.error = err;
382390
error.publish(context);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const dc = require('diagnostics_channel');
5+
const assert = require('assert');
6+
7+
const channel = dc.tracingChannel('test');
8+
9+
const expectedResult = { foo: 'bar' };
10+
const input = { foo: 'bar' };
11+
const thisArg = { baz: 'buz' };
12+
13+
function checkStart(found) {
14+
assert.strictEqual(found, input);
15+
}
16+
17+
function checkEnd(found) {
18+
checkStart(found);
19+
assert.strictEqual(found.error, undefined);
20+
assert.deepStrictEqual(found.result, expectedResult);
21+
}
22+
23+
const handlers = {
24+
start: common.mustCall(checkStart),
25+
end: common.mustCall(checkEnd),
26+
asyncStart: common.mustNotCall(),
27+
asyncEnd: common.mustNotCall(),
28+
error: common.mustNotCall()
29+
};
30+
31+
channel.subscribe(handlers);
32+
33+
process.on('warning', common.mustCall((warning) => {
34+
assert.strictEqual(
35+
warning.message,
36+
"tracePromise was called with the function '<anonymous>', which returned a non-thenable."
37+
);
38+
}));
39+
40+
assert.strictEqual(
41+
channel.tracePromise(common.mustCall(function(value) {
42+
assert.deepStrictEqual(this, thisArg);
43+
return value;
44+
}), input, thisArg, expectedResult),
45+
expectedResult,
46+
);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const dc = require('diagnostics_channel');
5+
const assert = require('assert');
6+
7+
class ResolvedThenable {
8+
#result;
9+
constructor(value) {
10+
this.#result = value;
11+
}
12+
then(resolve) {
13+
return new ResolvedThenable(resolve(this.#result));
14+
}
15+
}
16+
17+
const channel = dc.tracingChannel('test');
18+
19+
const expectedResult = { foo: 'bar' };
20+
const input = { foo: 'bar' };
21+
const thisArg = { baz: 'buz' };
22+
23+
function check(found) {
24+
assert.strictEqual(found, input);
25+
}
26+
27+
function checkAsync(found) {
28+
check(found);
29+
assert.strictEqual(found.error, undefined);
30+
assert.deepStrictEqual(found.result, expectedResult);
31+
}
32+
33+
const handlers = {
34+
start: common.mustCall(check),
35+
end: common.mustCall(check),
36+
asyncStart: common.mustCall(checkAsync),
37+
asyncEnd: common.mustCall(checkAsync),
38+
error: common.mustNotCall()
39+
};
40+
41+
channel.subscribe(handlers);
42+
43+
let innerThenable;
44+
45+
const result = channel.tracePromise(common.mustCall(function(value) {
46+
assert.deepStrictEqual(this, thisArg);
47+
innerThenable = new ResolvedThenable(value);
48+
return innerThenable;
49+
}), input, thisArg, expectedResult);
50+
51+
assert(result instanceof ResolvedThenable);
52+
assert.notStrictEqual(result, innerThenable);
53+
result.then(common.mustCall((value) => {
54+
assert.deepStrictEqual(value, expectedResult);
55+
}));

0 commit comments

Comments
 (0)