Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
33b9a33be6 fix(diagnostics_channel): retain channels with active subscribers
The WeakReference class in diagnostics_channel was allowing channels to
be garbage collected even when they had active subscribers. This caused
subscribers registered in preload scripts to be lost when the main
application script ran, breaking OpenTelemetry instrumentation for
frameworks like Fastify and Express.

The fix aligns WeakReference behavior with Node.js by maintaining a
strong reference when refCount > 0. When incRef() is called and refCount
transitions from 0 to 1, a strong reference is stored. When decRef()
returns refCount to 0, the strong reference is cleared, allowing GC.

The get() method now returns the strong reference when available,
falling back to the weak reference otherwise.

Fixes #26536

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 19:48:13 +00:00
2 changed files with 151 additions and 6 deletions

View File

@@ -16,20 +16,40 @@ const PromiseResolve = Promise.$resolve.bind(Promise);
const PromiseReject = Promise.$reject.bind(Promise);
const PromisePrototypeThen = (promise, onFulfilled, onRejected) => promise.then(onFulfilled, onRejected);
// TODO: https://github.com/nodejs/node/blob/fb47afc335ef78a8cef7eac52b8ee7f045300696/src/node_util.h#L13
class WeakReference<T extends WeakKey> extends WeakRef<T> {
#refs = 0;
// WeakReference that maintains a strong reference when refCount > 0.
// This ensures channels with active subscribers aren't garbage collected.
// See: https://github.com/nodejs/node/blob/fb47afc335ef78a8cef7eac52b8ee7f045300696/lib/internal/util.js#L888
class WeakReference<T extends WeakKey> {
#weak: WeakRef<T>;
#strong: T | null = null;
#refCount = 0;
constructor(object: T) {
this.#weak = new WeakRef(object);
}
get() {
return this.deref();
// Return strong reference if available (when refCount > 0), otherwise try weak
return this.#strong ?? this.#weak.deref();
}
incRef() {
return ++this.#refs;
this.#refCount++;
if (this.#refCount === 1) {
const derefed = this.#weak.deref();
if (derefed !== undefined) {
this.#strong = derefed;
}
}
return this.#refCount;
}
decRef() {
return --this.#refs;
this.#refCount--;
if (this.#refCount === 0) {
this.#strong = null;
}
return this.#refCount;
}
}

View File

@@ -0,0 +1,125 @@
// Test for https://github.com/oven-sh/bun/issues/26536
// diagnostics_channel subscribers should persist across preload and app scripts
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("diagnostics_channel subscribers persist from preload to main script", async () => {
// Create temp directory with test files
using dir = tempDir("issue-26536", {
"preload.mjs": `
import dc from 'node:diagnostics_channel';
const channel = dc.channel('test.channel.26536');
channel.subscribe((msg) => {
console.log("HOOK CALLED:", JSON.stringify(msg));
});
console.log("[preload] hasSubscribers:", channel.hasSubscribers);
`,
"app.mjs": `
import dc from 'node:diagnostics_channel';
const channel = dc.channel('test.channel.26536');
console.log("[app] hasSubscribers:", channel.hasSubscribers);
// Publish a message - should trigger the subscriber from preload
channel.publish({ test: true });
`,
});
// Run with preload
await using proc = Bun.spawn({
cmd: [bunExe(), "--preload", "./preload.mjs", "./app.mjs"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toContain("[preload] hasSubscribers: true");
expect(stdout).toContain("[app] hasSubscribers: true");
expect(stdout).toContain('HOOK CALLED: {"test":true}');
expect(exitCode).toBe(0);
});
test("diagnostics_channel subscribers persist with CJS preload", async () => {
// Create temp directory with test files
using dir = tempDir("issue-26536-cjs", {
"preload.cjs": `
const dc = require('node:diagnostics_channel');
const channel = dc.channel('test.channel.26536.cjs');
channel.subscribe((msg) => {
console.log("HOOK CALLED:", JSON.stringify(msg));
});
console.log("[preload] hasSubscribers:", channel.hasSubscribers);
`,
"app.mjs": `
import dc from 'node:diagnostics_channel';
const channel = dc.channel('test.channel.26536.cjs');
console.log("[app] hasSubscribers:", channel.hasSubscribers);
// Publish a message - should trigger the subscriber from preload
channel.publish({ fromApp: "hello" });
`,
});
// Run with CJS preload
await using proc = Bun.spawn({
cmd: [bunExe(), "--preload", "./preload.cjs", "./app.mjs"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toContain("[preload] hasSubscribers: true");
expect(stdout).toContain("[app] hasSubscribers: true");
expect(stdout).toContain('HOOK CALLED: {"fromApp":"hello"}');
expect(exitCode).toBe(0);
});
test("diagnostics_channel channel() returns same instance", async () => {
// Create temp directory with test files
using dir = tempDir("issue-26536-same-instance", {
"preload.mjs": `
import dc from 'node:diagnostics_channel';
const channel = dc.channel('test.channel.26536.same');
channel.subscribe(() => {});
// Store reference on globalThis
globalThis.__testChannel = channel;
console.log("[preload] stored channel");
`,
"app.mjs": `
import dc from 'node:diagnostics_channel';
const channel = dc.channel('test.channel.26536.same');
console.log("[app] same channel:", channel === globalThis.__testChannel);
`,
});
// Run with preload
await using proc = Bun.spawn({
cmd: [bunExe(), "--preload", "./preload.mjs", "./app.mjs"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toContain("[preload] stored channel");
expect(stdout).toContain("[app] same channel: true");
expect(exitCode).toBe(0);
});