Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 44 additions & 42 deletions dist/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69877,6 +69877,43 @@ function requireEnv(name) {
return value;
}

// src/retry.ts
var TransientError = class extends Error {
name = "TransientError";
};
function isTransient(err) {
if (err instanceof TransientError) return true;
if (!(err instanceof Error)) return false;
if (err.name === "AbortError" || err.name === "TimeoutError") return true;
const msg = err.message;
const http_match = /HTTP\D+(\d{3})/i.exec(msg);
if (http_match) {
const code = Number(http_match[1]);
return code >= 500 && code < 600;
}
return /ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|socket hang up/i.test(msg);
}
async function withRetry(fn, opts = {}) {
const retries = opts.retries ?? 1;
const min_delay = opts.minDelayMs ?? 500;
const jitter = opts.jitterMs ?? 500;
const classify = opts.isTransient ?? isTransient;
const sleep2 = opts.sleep ?? defaultSleep;
let attempt = 0;
for (; ; ) {
try {
return await fn();
} catch (err) {
if (attempt >= retries || !classify(err)) throw err;
attempt++;
await sleep2(min_delay + Math.floor(Math.random() * jitter));
}
}
}
function defaultSleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}

// src/common.ts
var VERSIONS_JSON = "https://ziglang.org/download/index.json";
var MACH_VERSIONS_JSON = "https://pkg.machengine.org/zig/index.json";
Expand Down Expand Up @@ -69970,11 +70007,13 @@ function compareReleaseParts(a, b) {
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
}
async function fetchJsonWithTimeout(url2, timeout_ms) {
const resp = await fetch(url2, { signal: AbortSignal.timeout(timeout_ms) });
if (!resp.ok) {
throw new Error(`Fetch ${url2} failed: HTTP ${resp.status}`);
}
return await resp.json();
return await withRetry(async () => {
const resp = await fetch(url2, { signal: AbortSignal.timeout(timeout_ms) });
if (!resp.ok) {
throw new Error(`Fetch ${url2} failed: HTTP ${resp.status}`);
}
return await resp.json();
});
}
async function getTarballName() {
const version3 = await getVersion2();
Expand Down Expand Up @@ -70196,43 +70235,6 @@ async function runAttempt(item, thunk, log2) {
}
}

// src/retry.ts
var TransientError = class extends Error {
name = "TransientError";
};
function isTransient(err) {
if (err instanceof TransientError) return true;
if (!(err instanceof Error)) return false;
if (err.name === "AbortError") return true;
const msg = err.message;
const http_match = /HTTP\D+(\d{3})/i.exec(msg);
if (http_match) {
const code = Number(http_match[1]);
return code >= 500 && code < 600;
}
return /ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|socket hang up/i.test(msg);
}
async function withRetry(fn, opts = {}) {
const retries = opts.retries ?? 1;
const min_delay = opts.minDelayMs ?? 500;
const jitter = opts.jitterMs ?? 500;
const classify = opts.isTransient ?? isTransient;
const sleep2 = opts.sleep ?? defaultSleep;
let attempt = 0;
for (; ; ) {
try {
return await fn();
} catch (err) {
if (attempt >= retries || !classify(err)) throw err;
attempt++;
await sleep2(min_delay + Math.floor(Math.random() * jitter));
}
}
}
function defaultSleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}

// src/source-tag.ts
function withSource(url2, tag) {
if (!tag) return url2;
Expand Down
49 changes: 44 additions & 5 deletions dist/post/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68725,6 +68725,43 @@ function errMessage(err) {
}
}

// src/retry.ts
var TransientError = class extends Error {
name = "TransientError";
};
function isTransient(err) {
if (err instanceof TransientError) return true;
if (!(err instanceof Error)) return false;
if (err.name === "AbortError" || err.name === "TimeoutError") return true;
const msg = err.message;
const http_match = /HTTP\D+(\d{3})/i.exec(msg);
if (http_match) {
const code = Number(http_match[1]);
return code >= 500 && code < 600;
}
return /ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|socket hang up/i.test(msg);
}
async function withRetry(fn, opts = {}) {
const retries = opts.retries ?? 1;
const min_delay = opts.minDelayMs ?? 500;
const jitter = opts.jitterMs ?? 500;
const classify = opts.isTransient ?? isTransient;
const sleep2 = opts.sleep ?? defaultSleep;
let attempt = 0;
for (; ; ) {
try {
return await fn();
} catch (err) {
if (attempt >= retries || !classify(err)) throw err;
attempt++;
await sleep2(min_delay + Math.floor(Math.random() * jitter));
}
}
}
function defaultSleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}

// src/common.ts
var VERSIONS_JSON = "https://ziglang.org/download/index.json";
var MACH_VERSIONS_JSON = "https://pkg.machengine.org/zig/index.json";
Expand Down Expand Up @@ -68818,11 +68855,13 @@ function compareReleaseParts(a, b) {
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
}
async function fetchJsonWithTimeout(url2, timeout_ms) {
const resp = await fetch(url2, { signal: AbortSignal.timeout(timeout_ms) });
if (!resp.ok) {
throw new Error(`Fetch ${url2} failed: HTTP ${resp.status}`);
}
return await resp.json();
return await withRetry(async () => {
const resp = await fetch(url2, { signal: AbortSignal.timeout(timeout_ms) });
if (!resp.ok) {
throw new Error(`Fetch ${url2} failed: HTTP ${resp.status}`);
}
return await resp.json();
});
}
async function getTarballName() {
const version3 = await getVersion2();
Expand Down
16 changes: 11 additions & 5 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
lookupMachVersion,
} from './schema.ts';
import { errMessage, isErrnoException } from './util.ts';
import { withRetry } from './retry.ts';

const VERSIONS_JSON = 'https://ziglang.org/download/index.json';
const MACH_VERSIONS_JSON = 'https://pkg.machengine.org/zig/index.json';
Expand Down Expand Up @@ -123,12 +124,17 @@ function compareReleaseParts(a: [number, number, number], b: [number, number, nu
return (a[0] - b[0]) || (a[1] - b[1]) || (a[2] - b[2]);
}

// Version resolution happens before any mirror is contacted, so a single
// slow response here fails the whole job with nothing else attempted. Retry
// transient failures (timeout, 5xx, connection reset) once.
async function fetchJsonWithTimeout(url: string, timeout_ms: number): Promise<unknown> {
const resp = await fetch(url, { signal: AbortSignal.timeout(timeout_ms) });
if (!resp.ok) {
throw new Error(`Fetch ${url} failed: HTTP ${resp.status}`);
}
return await resp.json();
return await withRetry(async () => {
const resp = await fetch(url, { signal: AbortSignal.timeout(timeout_ms) });
if (!resp.ok) {
throw new Error(`Fetch ${url} failed: HTTP ${resp.status}`);
}
return await resp.json();
});
}

export async function getTarballName(): Promise<string> {
Expand Down
4 changes: 3 additions & 1 deletion src/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export function isTransient(err: unknown): boolean {
if (!(err instanceof Error)) return false;
// AbortError sets err.name; the message ("The operation was aborted") does
// not mention "abort" in a way we can grep reliably across runtimes.
if (err.name === 'AbortError') return true;
// AbortSignal.timeout() rejects with a DOMException named TimeoutError
// rather than AbortError — a slow mirror is transient, so retry it too.
if (err.name === 'AbortError' || err.name === 'TimeoutError') return true;
const msg = err.message;
// @actions/tool-cache and friends emit "Unexpected HTTP response: 5xx";
// accept any non-digit run between "HTTP" and the status code.
Expand Down
12 changes: 12 additions & 0 deletions test/retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ test('isTransient: common network errnos are transient', () => {
assert.equal(isTransient(aborted), true);
});

test('isTransient: AbortSignal.timeout rejection is transient', async () => {
// The real shape a timed-out fetch produces: a DOMException named
// TimeoutError whose message mentions neither "abort" nor an errno.
const err = await new Promise<unknown>(resolve => {
AbortSignal.timeout(1).addEventListener('abort', function (this: AbortSignal) {
resolve(this.reason);
});
});
assert.equal((err as Error).name, 'TimeoutError');
assert.equal(isTransient(err), true);
});

test('isTransient: arbitrary application error is NOT transient', () => {
assert.equal(isTransient(new Error('signature verification failed')), false);
assert.equal(isTransient(new Error('filename mismatch')), false);
Expand Down
Loading