Skip to content

Commit

Permalink
chore: Avoid getport race conditions when starting anvil (#11077)
Browse files Browse the repository at this point in the history
Our test suite uses `startAnvil` all over the place to start an anvil
instance in the background. This was using `get-port`, which was subject
to race conditions when checking for port availability if we ran
multiple tests in parallel.

This PR removes `get-port` and instead uses port zero to ask the OS to
allocate a port for us, which should be free of race conditions (see
[this SO answer](https://unix.stackexchange.com/a/55918)).
  • Loading branch information
spalladino authored Jan 7, 2025
1 parent 649b590 commit b73f7f9
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 9 deletions.
1 change: 0 additions & 1 deletion yarn-project/ethereum/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"@aztec/l1-artifacts": "workspace:^",
"@viem/anvil": "^0.0.10",
"dotenv": "^16.0.3",
"get-port": "^7.1.0",
"tslib": "^2.4.0",
"viem": "^2.7.15",
"zod": "^3.23.8"
Expand Down
9 changes: 9 additions & 0 deletions yarn-project/ethereum/src/test/start_anvil.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ import { startAnvil } from './start_anvil.js';
describe('start_anvil', () => {
it('starts anvil on a free port', async () => {
const { anvil, rpcUrl } = await startAnvil();

const port = parseInt(new URL(rpcUrl).port);
expect(port).toBeLessThan(65536);
expect(port).toBeGreaterThan(1024);
expect(anvil.port).toEqual(port);

const host = new URL(rpcUrl).hostname;
expect(anvil.host).toEqual(host);

const publicClient = createPublicClient({ transport: http(rpcUrl) });
const chainId = await publicClient.getChainId();
expect(chainId).toEqual(31337);
Expand Down
25 changes: 17 additions & 8 deletions yarn-project/ethereum/src/test/start_anvil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,47 @@ import { makeBackoff, retry } from '@aztec/foundation/retry';
import { fileURLToPath } from '@aztec/foundation/url';

import { type Anvil, createAnvil } from '@viem/anvil';
import getPort from 'get-port';
import { dirname, resolve } from 'path';

/**
* Ensures there's a running Anvil instance and returns the RPC URL.
*/
export async function startAnvil(l1BlockTime?: number): Promise<{ anvil: Anvil; rpcUrl: string }> {
let ethereumHostPort: number | undefined;

const anvilBinary = resolve(dirname(fileURLToPath(import.meta.url)), '../../', 'scripts/anvil_kill_wrapper.sh');

let port: number | undefined;

// Start anvil.
// We go via a wrapper script to ensure if the parent dies, anvil dies.
const anvil = await retry(
async () => {
ethereumHostPort = await getPort();
const anvil = createAnvil({
anvilBinary,
port: ethereumHostPort,
port: 0,
blockTime: l1BlockTime,
stopTimeout: 1000,
});

// Listen to the anvil output to get the port.
const removeHandler = anvil.on('message', (message: string) => {
if (port === undefined && message.includes('Listening on')) {
port = parseInt(message.match(/Listening on ([^:]+):(\d+)/)![2]);
}
});
await anvil.start();
removeHandler();

return anvil;
},
'Start anvil',
makeBackoff([5, 5, 5]),
);

if (!ethereumHostPort) {
if (!port) {
throw new Error('Failed to start anvil');
}

const rpcUrl = `http://127.0.0.1:${ethereumHostPort}`;
return { anvil, rpcUrl };
// Monkeypatch the anvil instance to include the actually assigned port
Object.defineProperty(anvil, 'port', { value: port, writable: false });
return { anvil, rpcUrl: `http://127.0.0.1:${port}` };
}

0 comments on commit b73f7f9

Please sign in to comment.