From f1c4ba18dc33838ee59aa7fd619bc7e57e239c9d Mon Sep 17 00:00:00 2001 From: Pandelis Zembashis
Date: Tue, 27 Oct 2026 19:34:44 -0700 Subject: [PATCH] Allow negotiated Docker readiness within the shared deadline --- builder.mjs | 5 ++++- builder.test.mjs | 26 ++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/builder.mjs b/builder.mjs index 356729b..063b46d 100644 --- a/builder.mjs +++ b/builder.mjs @@ -205,11 +205,14 @@ export async function setup(env = process.env, run = command, wait = delay) { // the remote session. Retry this read only; never replay a build or push. // The control-plane wake is complete. Bound each native probe separately // so a stale connection cannot consume the entire readiness deadline. + // Docker negotiates its API with a ping before requesting the version. Each + // request establishes an authenticated tunnel; allow both to finish under + // load without restarting the shared 90-second readiness budget. const readinessDeadline = Date.now() + 90_000; for (let attempt = 0; attempt < 5; attempt++) { try { const version = await run('docker', ['--context', context, 'version', '--format', '{{.Server.Version}}'], dockerEnv, - Math.max(1, Math.min(15_000, readinessDeadline - Date.now()))); + Math.max(1, Math.min(45_000, readinessDeadline - Date.now()))); if (!/^\d+\.\d+\.\d+/.test(version.trim())) throw new Error('Remote Docker daemon is not ready'); break; } catch (error) { diff --git a/builder.test.mjs b/builder.test.mjs index 79e1215..cecffef 100644 --- a/builder.test.mjs +++ b/builder.test.mjs @@ -371,7 +371,7 @@ test('a stalled native connection uses one readiness budget and cannot restart t return f.run(exe, args, env, timeout); }; await assert.rejects(setup(f.env, run, async ms => waits.push(ms)), /connection reset/); - assert.deepEqual(timeouts, [15_000]); + assert.deepEqual(timeouts, [45_000]); assert.deepEqual(waits, []); await assert.rejects(readFile(f.env.GITHUB_OUTPUT), { code: 'ENOENT' }); }); @@ -389,7 +389,7 @@ test('a long shared wake completes before opening short-lived Docker probes', as } }); if (args.includes('version')) { assert.equal(now, 760_000); - assert.equal(timeout, 15_000); + assert.equal(timeout, 45_000); } return result; }; @@ -409,6 +409,28 @@ test('native probe timeouts are retried without repeating setup or a build', asy assert.equal(f.calls.filter(c => c.args[1] === 'setup').length, 1); }); +test('slow native negotiation can finish and retries share the remaining readiness budget', async t => { + const f = await fixture(t); + let now = 0; + t.mock.method(Date, 'now', () => now); + const timeouts = []; + const run = async (exe, args, env, timeout) => { + if (args.includes('version')) { + timeouts.push(timeout); + if (timeouts.length === 1) { + now += timeout; + throw new Error('timed out after 45s'); + } + // A healthy negotiation may take longer than the former 15s limit. + now += 25_000; + } + return f.run(exe, args, env, timeout); + }; + await setup(f.env, run, async ms => { now += ms; }); + assert.deepEqual(timeouts, [45_000, 43_000]); + assert.ok((await readFile(f.env.GITHUB_OUTPUT, 'utf8')).includes('driver<<')); +}); + test('command failures preserve timeout and signal details without stdout or environment', async () => { await assert.rejects(command(process.execPath, ['-e', "console.log('private-stdout'); setInterval(() => {}, 1000)"], { PRIVATE_TOKEN: 'private-env' }, 100), error => {