Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions dc-polyfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ if (!checks.hasChannelStoreMethods()) {

if (!checks.hasTracingChannel()) {
dc = require('./patch-tracing-channel.js')(dc);
} else if (checks.hasGarbageCollectionBug()) {
// Native tracingChannel exists but the underlying channel() registry is
// unstable; memoize tracingChannel by name so its sub-channel identities stay
// stable across calls.
dc = require('./patch-tracing-channel-stable-identity.js')(dc);
}

if (checks.hasSyncUnsubscribeBug()) {
Expand Down
36 changes: 36 additions & 0 deletions patch-tracing-channel-stable-identity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Make dc.tracingChannel(name) return a stable TracingChannel object per name
// on Node versions that have a native tracingChannel BUT also have the
// channel-registry GC bug (Node 16.17–16.x, 18.7–18.x, 20.0–20.5).
//
// On those versions the native tracingChannel(name) constructor calls Node's
// internal channel() to build its five sub-channels (start, end, asyncStart,
// asyncEnd, error). Because the underlying registry can return a brand-new
// Channel object for the same name on a subsequent call, two `tracingChannel(name)`
// invocations can end up with different sub-channel identities. Subscribers
// attached to one TracingChannel's sub-channels then miss publishes from
// another TracingChannel's sub-channels for the same name.
//
// patch-garbage-collection-bug.js memoizes `dc.channel(name)`, but
// native tracingChannel bypasses that wrapper — it goes through Node's
// internal channel() directly. Memoizing the TracingChannel by name closes
// the gap: same name returns the same TC (and therefore the same sub-channels)
// for the lifetime of the process.

module.exports = function (unpatched) {
const dc = { ...unpatched };
const original = dc.tracingChannel;
const byName = new Map();

dc.tracingChannel = function (nameOrChannels) {
if (typeof nameOrChannels !== 'string') {
// Object form — caller is providing explicit sub-channels; passthrough.
return original.call(this, nameOrChannels);
}
if (byName.has(nameOrChannels)) return byName.get(nameOrChannels);
const tc = original.call(this, nameOrChannels);
byName.set(nameOrChannels, tc);
return tc;
};

return dc;
};
90 changes: 90 additions & 0 deletions test/tracing-channel-stable-identity.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
const test = require('tape');
const patch = require('../patch-tracing-channel-stable-identity.js');

// Simulate the failure mode this patch addresses: native tracingChannel
// returns a fresh TracingChannel object on every string-form call (because
// Node's underlying channel() registry returned different sub-channel
// objects). The patch's job is to memoize TracingChannel objects by name
// so callers see a stable identity per name.
function mockUnpatched() {
const calls = { count: 0 };
function tracingChannel(nameOrChannels) {
calls.count++;
if (typeof nameOrChannels === 'string') {
// Mimic the native behavior of building fresh sub-channels per call,
// each with their own identity.
return {
_name: nameOrChannels,
_instanceId: calls.count,
start: { _name: `${nameOrChannels}:start`, _instanceId: calls.count },
end: { _name: `${nameOrChannels}:end`, _instanceId: calls.count },
asyncStart: { _name: `${nameOrChannels}:asyncStart`, _instanceId: calls.count },
asyncEnd: { _name: `${nameOrChannels}:asyncEnd`, _instanceId: calls.count },
error: { _name: `${nameOrChannels}:error`, _instanceId: calls.count },
};
}
return { _passthrough: true, channels: nameOrChannels };
}
return { tracingChannel, calls };
}

test('tracing-channel stable-identity patch: tracingChannel(name) returns same TracingChannel across calls', t => {
const { tracingChannel, calls } = mockUnpatched();
const dc = patch({ tracingChannel });

const a = dc.tracingChannel('foo');
const b = dc.tracingChannel('foo');
const c = dc.tracingChannel('foo');

t.strictEqual(a, b, 'second call returns same TracingChannel');
t.strictEqual(b, c, 'third call returns same TracingChannel');
t.strictEqual(a.start, b.start, 'sub-channel start identity stable');
t.strictEqual(a.end, b.end, 'sub-channel end identity stable');
t.strictEqual(a.asyncStart, b.asyncStart, 'sub-channel asyncStart identity stable');
t.strictEqual(a.asyncEnd, b.asyncEnd, 'sub-channel asyncEnd identity stable');
t.strictEqual(a.error, b.error, 'sub-channel error identity stable');

const callsAfterMemoization = calls.count;
dc.tracingChannel('foo');
dc.tracingChannel('foo');
t.equal(calls.count, callsAfterMemoization,
'memoized lookups do not re-invoke the underlying tracingChannel()');

t.end();
});

test('tracing-channel stable-identity patch: distinct names get distinct TracingChannels', t => {
const { tracingChannel } = mockUnpatched();
const dc = patch({ tracingChannel });

const foo = dc.tracingChannel('foo');
const bar = dc.tracingChannel('bar');

t.notStrictEqual(foo, bar, 'different names return different TracingChannels');
t.notStrictEqual(foo.start, bar.start, 'different names have different sub-channels');
t.strictEqual(dc.tracingChannel('foo'), foo, 'foo memoization holds');
t.strictEqual(dc.tracingChannel('bar'), bar, 'bar memoization holds');
t.end();
});

test('tracing-channel stable-identity patch: object form is passthrough (not memoized)', t => {
const { tracingChannel } = mockUnpatched();
const dc = patch({ tracingChannel });

const channels = {
start: { _name: 'custom:start' },
end: { _name: 'custom:end' },
asyncStart: { _name: 'custom:asyncStart' },
asyncEnd: { _name: 'custom:asyncEnd' },
error: { _name: 'custom:error' },
};

const a = dc.tracingChannel(channels);
const b = dc.tracingChannel(channels);

// Object form bypasses memoization — caller is providing explicit channels.
t.notStrictEqual(a, b, 'object form returns a fresh wrapper each time');
t.ok(a._passthrough, 'first call passes through');
t.ok(b._passthrough, 'second call passes through');
t.end();
});
Loading