Commit f9defa61fa9 for nodejs
commit f9defa61fa95bcce4fa2e04b1204ca703d6604a2
Author: Filip Skokan <panva.ip@gmail.com>
Date: Thu Oct 8 11:58:29 2026 +0200
lib: fix benchmark record acknowledgement race
The child can receive an acknowledgement and send its next record before
the parent's send callback runs. Clear recordPending before sending the
acknowledgement to avoid rejecting that record as an invalid sequence.
Add a regression test that delays send callbacks until the child sends
its next record or closes.
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/66430
Reviewed-By: James M Snell <jasnell@gmail.com>
diff --git a/lib/internal/bench_runner/cli.js b/lib/internal/bench_runner/cli.js
index 48af55c9cf1..5884046e146 100644
--- a/lib/internal/bench_runner/cli.js
+++ b/lib/internal/bench_runner/cli.js
@@ -956,10 +956,11 @@ async function runChild(path, options, scope, onRecord) {
}
const pending = handleRecord(record);
if (protocolError === undefined) {
- const acknowledged = PromisePrototypeThen(
- PromiseResolve(pending), () => sendAck(child, message.id));
- trackPending(PromisePrototypeThen(acknowledged, () => {
+ trackPending(PromisePrototypeThen(PromiseResolve(pending), () => {
+ // The child can receive the acknowledgement and send its next
+ // record before the send callback runs.
recordPending = false;
+ return sendAck(child, message.id);
}));
}
} catch (error) {
diff --git a/test/fixtures/bench-runner/delayed-ack.cjs b/test/fixtures/bench-runner/delayed-ack.cjs
new file mode 100644
index 00000000000..3d439aa579f
--- /dev/null
+++ b/test/fixtures/bench-runner/delayed-ack.cjs
@@ -0,0 +1,31 @@
+'use strict';
+
+const common = require('../../common');
+const childProcess = require('child_process');
+const { spawn } = childProcess;
+
+childProcess.spawn = (...args) => {
+ const child = spawn(...args);
+ const { send } = child;
+ child.send = function(message, handle, options, callback) {
+ if (message?.type === 'node:bench:ack') {
+ // Report send completion only after the child has responded to the ack.
+ const onComplete = common.mustCall(() => {
+ child.removeListener('message', onComplete);
+ child.removeListener('close', onComplete);
+ callback(null);
+ });
+ child.once('message', onComplete);
+ child.once('close', onComplete);
+ return send.call(this, message, handle, options, (error) => {
+ if (error) {
+ child.removeListener('message', onComplete);
+ child.removeListener('close', onComplete);
+ callback(error);
+ }
+ });
+ }
+ return send.call(this, message, handle, options, callback);
+ };
+ return child;
+};
diff --git a/test/parallel/test-bench-cli-ack.js b/test/parallel/test-bench-cli-ack.js
new file mode 100644
index 00000000000..99bf9b58b27
--- /dev/null
+++ b/test/parallel/test-bench-cli-ack.js
@@ -0,0 +1,23 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const { spawnSync } = require('child_process');
+const fixtures = require('../common/fixtures');
+
+const result = spawnSync(process.execPath, [
+ '--no-warnings', '--experimental-bench', '--bench',
+ '--require', fixtures.path('bench-runner/delayed-ack.cjs'),
+ '--bench-reporter=json',
+ fixtures.path('bench-runner/acknowledged-records.mjs'),
+], { encoding: 'utf8', timeout: common.platformTimeout(30_000) });
+
+assert.ifError(result.error);
+assert.strictEqual(result.signal, null);
+assert.strictEqual(result.status, 0, result.stdout + result.stderr);
+assert.strictEqual(result.stderr, '');
+const records = result.stdout.trim().split('\n').map((line) => JSON.parse(line));
+const diagnostics = records.filter(({ type }) => type === 'bench:diagnostic');
+assert.strictEqual(diagnostics.length, 32);
+assert(diagnostics.every(({ data }) => /^acknowledged 10\d{3}$/.test(data.message)));
+assert.strictEqual(records.at(-1).data.success, true);