Skip to content

Commit 2413b09

Browse files
authored
fix: don't time out bb socket startup while the bb process is alive (#24802)
Fixes the `Error: Timeout connecting to bb socket: unknown (retry=false)` failures reported by an operator since v5.0.0, which killed proving jobs during epoch top-tree and cost epochs. ## Root cause The NativeUnixSocket backend gave bb a hard 5s wall-clock budget that was **shared** between two phases: waiting for bb to create its socket file, and connecting to it. `connectWithRetry` reused the `startTime` captured before the file-wait poll loop, so when bb took close to 5s to create the socket (many bb processes spawning simultaneously during top-tree checkpoint/merge jobs, or the Node event loop starving the 50ms polls), the connect phase was entered with its budget already exhausted and threw without making a single connect attempt — that is what the `unknown` in the error message means (`lastErr` was never set). Two aggravating factors: - 5s is an arbitrary opinion about how fast a loaded machine should spawn a process. The pre-socket v4 CLI model had no such deadline — you waited on the child, and the only failures were real process events. - The resulting plain `Error` reached the proving agent, which only honours the retry flag on `ProvingError`s, so the failure was reported `retry=false` and the job failed permanently instead of being re-enqueued. ## Changes ### `barretenberg/ts` — socket backend restructure (`native_socket.ts`) - Replaced the constructor + deferred `connectionPromise` wiring with a `static async new()` factory, matching the shm and wasm backends. An instance can now only exist once connected, so `call()` no longer awaits a stashed connection promise (and is no longer `async` — its body has no awaits). - Startup is one flat wait-and-connect loop with the correct liveness condition: **retry for as long as the bb process is alive**. If bb dies, fail immediately with the real cause (exit code / signal); spawn failures are caught up front by awaiting the `spawn` event. Both 5s timers are deleted. - One generous 60s backstop remains for a bb that is alive but wedged before `listen()`. It kills the process (routing cleanup through the exit path) rather than leaving an orphan. It is a broken-process detector, not a performance expectation: the timed window ends at bb's `listen()`, which is reached after only exec + linking + minimal init (the expensive startup work comes after the socket is up), so firing it requires a machine degraded far beyond ordinary proving load. And if it ever does fire on a merely-distressed machine, the failure is retryable (see below), so the cost is a re-enqueue, not an epoch. - The four copy-pasted reject-pending-callbacks blocks (process error/exit, socket error/end) are consolidated into `failAllPending()`. Note one deliberate behavioural improvement: the socket `error`/`end` handlers now also destroy and null the socket, so subsequent `call()`s fail fast with `Socket not connected` instead of `write after destroy`. - New `native_socket.test.ts` covers: prompt startup, bb taking >5s to create its socket (the incident's failure mode — fails on the old code by construction, passes now), bb dying before the socket exists, and a nonexistent binary. ### `yarn-project/bb-prover` — make startup failures retryable - `BBJsInstance.create` wraps any `Barretenberg.new` failure as `ProvingError(..., retry: true)`: bb startup failures are environmental (machine load, wedged process), never a property of the proof inputs, so the job is always safe to retry. - The two catch sites in `bb_prover.ts` that re-wrap errors (`generateProof`, `verifyProof`) previously constructed a fresh `ProvingError` with the default `retry=false`, silently dropping the flag. They now propagate the inner error's retryability and attach it as `cause`. Errors that were non-retryable before remain non-retryable. The AVM paths don't wrap, so they needed no change. - New `bb_js_backend.test.ts` asserts a startup failure surfaces as a retryable `ProvingError`. ## Result for operators A loaded prover can take as long as it needs to spawn bb; a genuinely broken bb fails after 60s with a concrete, diagnosable cause instead of `Timeout ... unknown`; and if that ever happens the broker re-enqueues the job (up to its retry limit) instead of failing it permanently and costing the epoch. ## Testing - `barretenberg/ts`: new jest suite passes (including a 7s-delayed fake bb that the old implementation fails on). - `yarn-project`: full `yarn build`, bb-prover lint, and the new unit test pass. Root `./bootstrap.sh` green. ## Unrelated CI fix included `docs/examples/ts/aztecjs_runner/run.sh` installed `typescript` and `tsx` unpinned; the TypeScript 7.0.2 release (which drops `lib/_tsc.js`) crashes Yarn 4's builtin `compat/typescript` patch at install time, failing `docs/examples/bootstrap.sh execute` on every branch. Pinned to `typescript@^5.3.3` / `tsx@^4`, matching the existing pin in `docs/examples/ts/bootstrap.sh`. This was the only failure in this PR's first full CI run and needs forward-porting to the `next` line as well.
2 parents aaee37c + 9f914e1 commit 2413b09

6 files changed

Lines changed: 230 additions & 199 deletions

File tree

barretenberg/ts/src/bb_backends/node/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ export async function createAsyncBackend(
2626
throw new Error('Native backend requires bb binary.');
2727
}
2828
logger(`Using native Unix socket backend: ${bbPath}`);
29-
const socket = new BarretenbergNativeSocketAsyncBackend(bbPath, options.threads, options.logger, options.unref);
29+
const socket = await BarretenbergNativeSocketAsyncBackend.new(
30+
bbPath,
31+
options.threads,
32+
options.logger,
33+
options.unref,
34+
);
3035
return new Barretenberg(socket, options);
3136
}
3237

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { jest } from '@jest/globals';
2+
import * as fs from 'fs';
3+
import * as os from 'os';
4+
import * as path from 'path';
5+
import { BarretenbergNativeSocketAsyncBackend } from './native_socket.js';
6+
7+
jest.setTimeout(30_000);
8+
9+
// Echo server speaking the bb msgpack socket protocol (4-byte LE length prefix), started after
10+
// an optional delay to simulate bb's startup time on a loaded machine.
11+
const ECHO_SERVER_JS = `
12+
const net = require('net');
13+
const socketPath = process.argv[2];
14+
const server = net.createServer(sock => {
15+
let buf = Buffer.alloc(0);
16+
sock.on('data', d => {
17+
buf = Buffer.concat([buf, d]);
18+
while (buf.length >= 4) {
19+
const len = buf.readUInt32LE(0);
20+
if (buf.length < 4 + len) break;
21+
const payload = buf.subarray(4, 4 + len);
22+
const out = Buffer.alloc(4);
23+
out.writeUInt32LE(payload.length, 0);
24+
sock.write(out);
25+
sock.write(payload);
26+
buf = buf.subarray(4 + len);
27+
}
28+
});
29+
});
30+
server.listen(socketPath);
31+
`;
32+
33+
// A fake bb binary: a bash script that optionally sleeps, then runs the echo server on the
34+
// socket path bb receives via `msgpack run --input <path>` ($4).
35+
function writeFakeBb(startupDelaySecs: number): string {
36+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-bb-'));
37+
const serverJs = path.join(dir, 'echo_server.cjs');
38+
fs.writeFileSync(serverJs, ECHO_SERVER_JS);
39+
const file = path.join(dir, 'bb');
40+
const sleep = startupDelaySecs > 0 ? `sleep ${startupDelaySecs}\n` : '';
41+
fs.writeFileSync(file, `#!/bin/bash\n${sleep}exec node ${serverJs} "$4"\n`, { mode: 0o755 });
42+
return file;
43+
}
44+
45+
function writeFakeBbScript(script: string): string {
46+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-bb-'));
47+
const file = path.join(dir, 'bb');
48+
fs.writeFileSync(file, script, { mode: 0o755 });
49+
return file;
50+
}
51+
52+
describe('BarretenbergNativeSocketAsyncBackend', () => {
53+
it('connects and echoes when bb starts promptly', async () => {
54+
const fakeBb = writeFakeBb(0);
55+
const backend = await BarretenbergNativeSocketAsyncBackend.new(fakeBb);
56+
const response = await backend.call(new Uint8Array([1, 2, 3, 4]));
57+
expect(response).toEqual(new Uint8Array([1, 2, 3, 4]));
58+
await backend.destroy();
59+
});
60+
61+
it('connects even when bb takes longer than 5s to create its socket', async () => {
62+
const fakeBb = writeFakeBb(7);
63+
const backend = await BarretenbergNativeSocketAsyncBackend.new(fakeBb);
64+
const response = await backend.call(new Uint8Array([42]));
65+
expect(response).toEqual(new Uint8Array([42]));
66+
await backend.destroy();
67+
});
68+
69+
it('fails with the exit cause when bb dies before creating its socket', async () => {
70+
const fakeBb = writeFakeBbScript(`#!/bin/bash\nexit 17\n`);
71+
await expect(BarretenbergNativeSocketAsyncBackend.new(fakeBb)).rejects.toThrow(
72+
/exited before socket connection was established \(code=17/,
73+
);
74+
});
75+
76+
it('fails with the spawn error when the bb binary does not exist', async () => {
77+
await expect(BarretenbergNativeSocketAsyncBackend.new('/nonexistent/bb-binary')).rejects.toThrow(
78+
/Native backend process error/,
79+
);
80+
});
81+
});

0 commit comments

Comments
 (0)