Skip to content

Commit 1becba6

Browse files
committed
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
1 parent cede7e6 commit 1becba6

3 files changed

Lines changed: 58 additions & 3 deletions

File tree

‎lib/internal/bench_runner/cli.js‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -954,10 +954,11 @@ async function runChild(path, options, scope, onRecord) {
954954
}
955955
const pending = handleRecord(record);
956956
if (protocolError === undefined) {
957-
const acknowledged = PromisePrototypeThen(
958-
PromiseResolve(pending), () => sendAck(child, message.id));
959-
trackPending(PromisePrototypeThen(acknowledged, () => {
957+
trackPending(PromisePrototypeThen(PromiseResolve(pending), () => {
958+
// The child can receive the acknowledgement and send its next
959+
// record before the send callback runs.
960960
recordPending = false;
961+
return sendAck(child, message.id);
961962
}));
962963
}
963964
} catch (error) {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict';
2+
3+
const common = require('../../common');
4+
const childProcess = require('child_process');
5+
const { spawn } = childProcess;
6+
7+
childProcess.spawn = (...args) => {
8+
const child = spawn(...args);
9+
const { send } = child;
10+
child.send = function(message, handle, options, callback) {
11+
if (message?.type === 'node:bench:ack') {
12+
// Report send completion only after the child has responded to the ack.
13+
const onComplete = common.mustCall(() => {
14+
child.removeListener('message', onComplete);
15+
child.removeListener('close', onComplete);
16+
callback(null);
17+
});
18+
child.once('message', onComplete);
19+
child.once('close', onComplete);
20+
return send.call(this, message, handle, options, (error) => {
21+
if (error) {
22+
child.removeListener('message', onComplete);
23+
child.removeListener('close', onComplete);
24+
callback(error);
25+
}
26+
});
27+
}
28+
return send.call(this, message, handle, options, callback);
29+
};
30+
return child;
31+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { spawnSync } = require('child_process');
6+
const fixtures = require('../common/fixtures');
7+
8+
const result = spawnSync(process.execPath, [
9+
'--no-warnings', '--experimental-bench', '--bench',
10+
'--require', fixtures.path('bench-runner/delayed-ack.cjs'),
11+
'--bench-reporter=json',
12+
fixtures.path('bench-runner/acknowledged-records.mjs'),
13+
], { encoding: 'utf8', timeout: common.platformTimeout(30_000) });
14+
15+
assert.ifError(result.error);
16+
assert.strictEqual(result.signal, null);
17+
assert.strictEqual(result.status, 0, result.stdout + result.stderr);
18+
assert.strictEqual(result.stderr, '');
19+
const records = result.stdout.trim().split('\n').map((line) => JSON.parse(line));
20+
const diagnostics = records.filter(({ type }) => type === 'bench:diagnostic');
21+
assert.strictEqual(diagnostics.length, 32);
22+
assert(diagnostics.every(({ data }) => /^acknowledged 10\d{3}$/.test(data.message)));
23+
assert.strictEqual(records.at(-1).data.success, true);

0 commit comments

Comments
 (0)