mirror of
https://github.com/astral-sh/setup-uv.git
synced 2026-09-03 22:29:21 +00:00
Tolerate transient manifest timeouts (#1016)
Transient timeout fetching manifests have increased significantly recently, especially with private runners. ``` Fetching manifest data from https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson ... Error: The operation was aborted due to timeout ``` Retry transient manifest network failures up to three times with a progressive backoff (not exponential), keeping the total wait bounded while making setup resilient to short network blips. Co-authored-by: Raymond <arguile-@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,11 @@
|
|||||||
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
import {
|
||||||
|
afterEach,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
jest,
|
||||||
|
} from "@jest/globals";
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
|
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
|
||||||
const mockFetch = jest.fn<any>();
|
const mockFetch = jest.fn<any>();
|
||||||
@@ -13,6 +20,7 @@ jest.unstable_mockModule("../../src/utils/fetch", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
MANIFEST_FETCH_ATTEMPTS,
|
||||||
clearManifestCache,
|
clearManifestCache,
|
||||||
fetchManifest,
|
fetchManifest,
|
||||||
getAllVersions,
|
getAllVersions,
|
||||||
@@ -73,6 +81,10 @@ describe("manifest", () => {
|
|||||||
mockFetch.mockReset();
|
mockFetch.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
describe("fetchManifest", () => {
|
describe("fetchManifest", () => {
|
||||||
it("fetches and parses manifest data", async () => {
|
it("fetches and parses manifest data", async () => {
|
||||||
mockFetch.mockResolvedValue(
|
mockFetch.mockResolvedValue(
|
||||||
@@ -86,6 +98,34 @@ describe("manifest", () => {
|
|||||||
expect(versions[1]?.version).toBe("0.9.25");
|
expect(versions[1]?.version).toBe("0.9.25");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("retries network failures", async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
mockFetch
|
||||||
|
.mockRejectedValueOnce(new Error("request timed out"))
|
||||||
|
.mockResolvedValueOnce(
|
||||||
|
createMockResponse(true, 200, "OK", sampleManifestResponse),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = fetchManifest();
|
||||||
|
await jest.runAllTimersAsync();
|
||||||
|
|
||||||
|
await expect(result).resolves.toHaveLength(2);
|
||||||
|
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stops after the configured number of network failures", async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
mockFetch.mockRejectedValue(new Error("request timed out"));
|
||||||
|
|
||||||
|
const result = expect(fetchManifest()).rejects.toThrow(
|
||||||
|
"request timed out",
|
||||||
|
);
|
||||||
|
await jest.runAllTimersAsync();
|
||||||
|
|
||||||
|
await result;
|
||||||
|
expect(mockFetch).toHaveBeenCalledTimes(MANIFEST_FETCH_ATTEMPTS);
|
||||||
|
});
|
||||||
|
|
||||||
it("throws on a failed fetch", async () => {
|
it("throws on a failed fetch", async () => {
|
||||||
mockFetch.mockResolvedValue(
|
mockFetch.mockResolvedValue(
|
||||||
createMockResponse(false, 500, "Internal Server Error", ""),
|
createMockResponse(false, 500, "Internal Server Error", ""),
|
||||||
|
|||||||
+18
-1
@@ -99724,6 +99724,7 @@ function formatVariants(entries) {
|
|||||||
|
|
||||||
// src/download/manifest.ts
|
// src/download/manifest.ts
|
||||||
var cachedManifestData = /* @__PURE__ */ new Map();
|
var cachedManifestData = /* @__PURE__ */ new Map();
|
||||||
|
var MANIFEST_FETCH_ATTEMPTS = 3;
|
||||||
async function fetchManifest(manifestUrl = VERSIONS_MANIFEST_URL) {
|
async function fetchManifest(manifestUrl = VERSIONS_MANIFEST_URL) {
|
||||||
const cachedManifest = cachedManifestData.get(manifestUrl);
|
const cachedManifest = cachedManifestData.get(manifestUrl);
|
||||||
if (cachedManifest?.complete === true) {
|
if (cachedManifest?.complete === true) {
|
||||||
@@ -99802,8 +99803,24 @@ async function getArtifact(version3, arch3, platform2, manifestUrl = VERSIONS_MA
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
async function fetchManifestResponse(manifestUrl) {
|
async function fetchManifestResponse(manifestUrl) {
|
||||||
|
let response;
|
||||||
|
for (let attempt = 1; attempt <= MANIFEST_FETCH_ATTEMPTS; attempt++) {
|
||||||
info2(`Fetching manifest data from ${manifestUrl} ...`);
|
info2(`Fetching manifest data from ${manifestUrl} ...`);
|
||||||
const response = await fetch(manifestUrl, {});
|
try {
|
||||||
|
response = await fetch(manifestUrl, {});
|
||||||
|
break;
|
||||||
|
} catch (error2) {
|
||||||
|
if (attempt >= MANIFEST_FETCH_ATTEMPTS) {
|
||||||
|
throw error2;
|
||||||
|
}
|
||||||
|
const delayMs = 1e3 * 2 ** (attempt - 1);
|
||||||
|
info2(`Manifest fetch failed; retrying in ${delayMs}ms ...`);
|
||||||
|
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (response === void 0) {
|
||||||
|
throw new Error("Manifest fetch attempts exhausted.");
|
||||||
|
}
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to fetch manifest data: ${response.status} ${response.statusText}`
|
`Failed to fetch manifest data: ${response.status} ${response.statusText}`
|
||||||
|
|||||||
+18
-1
@@ -52390,6 +52390,7 @@ function info2(msg) {
|
|||||||
|
|
||||||
// src/download/manifest.ts
|
// src/download/manifest.ts
|
||||||
var cachedManifestData = /* @__PURE__ */ new Map();
|
var cachedManifestData = /* @__PURE__ */ new Map();
|
||||||
|
var MANIFEST_FETCH_ATTEMPTS = 3;
|
||||||
async function fetchManifest(manifestUrl = VERSIONS_MANIFEST_URL) {
|
async function fetchManifest(manifestUrl = VERSIONS_MANIFEST_URL) {
|
||||||
const cachedManifest = cachedManifestData.get(manifestUrl);
|
const cachedManifest = cachedManifestData.get(manifestUrl);
|
||||||
if (cachedManifest?.complete === true) {
|
if (cachedManifest?.complete === true) {
|
||||||
@@ -52430,8 +52431,24 @@ async function getLatestVersion(manifestUrl = VERSIONS_MANIFEST_URL) {
|
|||||||
return latestVersion;
|
return latestVersion;
|
||||||
}
|
}
|
||||||
async function fetchManifestResponse(manifestUrl) {
|
async function fetchManifestResponse(manifestUrl) {
|
||||||
|
let response;
|
||||||
|
for (let attempt = 1; attempt <= MANIFEST_FETCH_ATTEMPTS; attempt++) {
|
||||||
info2(`Fetching manifest data from ${manifestUrl} ...`);
|
info2(`Fetching manifest data from ${manifestUrl} ...`);
|
||||||
const response = await fetch(manifestUrl, {});
|
try {
|
||||||
|
response = await fetch(manifestUrl, {});
|
||||||
|
break;
|
||||||
|
} catch (error2) {
|
||||||
|
if (attempt >= MANIFEST_FETCH_ATTEMPTS) {
|
||||||
|
throw error2;
|
||||||
|
}
|
||||||
|
const delayMs = 1e3 * 2 ** (attempt - 1);
|
||||||
|
info2(`Manifest fetch failed; retrying in ${delayMs}ms ...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (response === void 0) {
|
||||||
|
throw new Error("Manifest fetch attempts exhausted.");
|
||||||
|
}
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to fetch manifest data: ${response.status} ${response.statusText}`
|
`Failed to fetch manifest data: ${response.status} ${response.statusText}`
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ interface CachedManifest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cachedManifestData = new Map<string, CachedManifest>();
|
const cachedManifestData = new Map<string, CachedManifest>();
|
||||||
|
export const MANIFEST_FETCH_ATTEMPTS = 3;
|
||||||
|
|
||||||
export async function fetchManifest(
|
export async function fetchManifest(
|
||||||
manifestUrl: string = VERSIONS_MANIFEST_URL,
|
manifestUrl: string = VERSIONS_MANIFEST_URL,
|
||||||
@@ -166,8 +167,26 @@ export function clearManifestCache(manifestUrl?: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchManifestResponse(manifestUrl: string) {
|
async function fetchManifestResponse(manifestUrl: string) {
|
||||||
|
let response: Awaited<ReturnType<typeof fetch>> | undefined;
|
||||||
|
for (let attempt = 1; attempt <= MANIFEST_FETCH_ATTEMPTS; attempt++) {
|
||||||
log.info(`Fetching manifest data from ${manifestUrl} ...`);
|
log.info(`Fetching manifest data from ${manifestUrl} ...`);
|
||||||
const response = await fetch(manifestUrl, {});
|
try {
|
||||||
|
response = await fetch(manifestUrl, {});
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt >= MANIFEST_FETCH_ATTEMPTS) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const delayMs = 1_000 * 2 ** (attempt - 1);
|
||||||
|
log.info(`Manifest fetch failed; retrying in ${delayMs}ms ...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response === undefined) {
|
||||||
|
throw new Error("Manifest fetch attempts exhausted.");
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to fetch manifest data: ${response.status} ${response.statusText}`,
|
`Failed to fetch manifest data: ${response.status} ${response.statusText}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user