Skip to content

Commit d70c3e8

Browse files
committed
fix(init): bound waitForRpc per-request so wait-l1 can't hang forever
waitForRpc looped on a 60s deadline but awaited fetch() with no per-request timeout. Docker's published port accepts TCP before anvil serves HTTP, so a single fetch could block indefinitely; the loop never re-checked the deadline and the 60s timeout never fired. In CI this hung wait-l1 for the full 75min job timeout (3 of 4 first-wave publish jobs), defeating the init retry loop (which only recovers from init exiting, not hanging). Add signal: AbortSignal.timeout(pollIntervalMs) to the fetch so a hung request aborts and the loop enforces its deadline, throwing the existing 'not ready' error. Fixes wait-l1/l2/l3 alike. Regression test stubs a never-resolving fetch and asserts waitForRpc rejects within its timeout. Defense-in-depth: wrap each CI init attempt in 'timeout 1500' so any future unforeseen hang is bounded and the retry loop's reset can recover.
1 parent 01308c1 commit d70c3e8

3 files changed

Lines changed: 30 additions & 2 deletions

File tree

.github/workflows/release-testnode-image.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,16 @@ jobs:
160160
for attempt in $(seq 1 "$max"); do
161161
echo "::group::init attempt $attempt/$max ($SNAPSHOT_ID)"
162162
# shellcheck disable=SC2086
163-
pnpm dev init \
163+
# Bound each attempt so an unforeseen hang can't consume the whole
164+
# job; 1500s comfortably exceeds a healthy ~10-15min init and keeps
165+
# 3 attempts within timeout-minutes: 75. The retry loop resets with
166+
# `docker compose ... down -v` below so a timed-out attempt recovers.
167+
timeout 1500 pnpm dev init \
164168
--nitro-contracts-version "$NC_VERSION" \
165169
$EXTRA \
166170
--capture-id "$SNAPSHOT_ID" \
167171
--rebuild \
168-
--skip-post-capture-verify || echo "init exited non-zero on attempt $attempt"
172+
--skip-post-capture-verify || echo "init exited/timed out on attempt $attempt"
169173
echo "::endgroup::"
170174
if [ -f "$MANIFEST" ]; then
171175
echo "snapshot captured on attempt $attempt"

packages/core/src/docker.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ export async function waitForRpc(
9191
method: "eth_chainId",
9292
params: [],
9393
}),
94+
// Bound each request so a hung connection (e.g. docker-proxy
95+
// accepting TCP before anvil serves HTTP) aborts and lets the
96+
// loop re-check the deadline instead of blocking forever.
97+
signal: AbortSignal.timeout(pollIntervalMs),
9498
});
9599
if (response.ok) {
96100
return;

packages/core/test/docker.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,24 @@ describe("waitForRpc", () => {
169169
await expect(waitForRpc("http://localhost:8545", 5000, 10)).resolves.toBeUndefined();
170170
expect(fetchSpy).toHaveBeenCalledTimes(3);
171171
});
172+
173+
it("rejects on overall timeout even when individual fetches never resolve", async () => {
174+
// Simulate docker-proxy accepting the TCP connection but never serving
175+
// HTTP: fetch connects and never resolves on its own, mirroring real
176+
// fetch by rejecting only when its abort signal fires. Without a
177+
// per-request signal (the bug) every call hangs forever and the loop
178+
// never re-checks the deadline.
179+
fetchSpy.mockImplementation(
180+
(_url: string, init?: RequestInit) =>
181+
new Promise((_resolve, reject) => {
182+
init?.signal?.addEventListener("abort", () =>
183+
reject(new DOMException("The operation was aborted.", "AbortError")),
184+
);
185+
}),
186+
);
187+
188+
await expect(waitForRpc("http://localhost:8545", 200, 20)).rejects.toThrow(
189+
"RPC at http://localhost:8545 not ready",
190+
);
191+
});
172192
});

0 commit comments

Comments
 (0)