Skip to content

Commit b18a0ee

Browse files
committed
feat: implement backoff retry for install subcommand downloads
1 parent a25f899 commit b18a0ee

1 file changed

Lines changed: 62 additions & 16 deletions

File tree

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,78 @@
1-
import { createWriteStream } from "node:fs";
1+
import { createWriteStream, rmSync } from "node:fs";
22
import { pipeline } from "node:stream/promises";
33
import { EnvHttpProxyAgent, fetch } from "undici";
44
import type { Asset } from "./types";
55
import { ERR_INSTALL_FAILED_TO_DOWNLOAD_ASSET } from "../../errors";
66

77
const dispatcher = new EnvHttpProxyAgent();
88

9+
const DOWNLOAD_BACKOFFS = [500, 1000, 2000, 5000, 8000];
10+
11+
function isRetryableDownloadError(error: unknown): boolean {
12+
if (!(error instanceof Error)) {
13+
return false;
14+
}
15+
16+
const code = "code" in error ? error.code : undefined;
17+
18+
return [
19+
"ECONNRESET",
20+
"ECONNREFUSED",
21+
"ECONNABORTED",
22+
"ETIMEDOUT",
23+
"EAI_AGAIN",
24+
].includes(code as string);
25+
}
26+
27+
function delay(ms: number): Promise<void> {
28+
return new Promise((resolve) => setTimeout(resolve, ms));
29+
}
30+
931
export async function downloadAsset(
1032
asset: Asset,
1133
source: string,
1234
destination: string,
1335
): Promise<void> {
14-
const response = await fetch(source, {
15-
dispatcher,
16-
});
36+
let lastError: unknown;
1737

18-
if (!response.ok || !response.body) {
19-
throw new Error(
20-
`${ERR_INSTALL_FAILED_TO_DOWNLOAD_ASSET}: ${asset.asset} (${response.status} ${response.statusText})`,
21-
);
22-
}
38+
for (let attempt = 0; attempt <= DOWNLOAD_BACKOFFS.length; attempt++) {
39+
try {
40+
const response = await fetch(source, {
41+
dispatcher,
42+
});
2343

24-
try {
25-
await pipeline(response.body, createWriteStream(destination));
26-
} catch (cause) {
27-
throw new Error(
28-
`${ERR_INSTALL_FAILED_TO_DOWNLOAD_ASSET}: ${asset.asset} (${response.status} ${response.statusText})`,
29-
{ cause },
30-
);
44+
if (!response.ok || !response.body) {
45+
if (response.status >= 500 && attempt < DOWNLOAD_BACKOFFS.length) {
46+
await delay(DOWNLOAD_BACKOFFS[attempt]);
47+
48+
continue;
49+
}
50+
51+
throw new Error(
52+
`${ERR_INSTALL_FAILED_TO_DOWNLOAD_ASSET}: ${asset.asset} (${response.status} ${response.statusText})`,
53+
);
54+
}
55+
56+
await pipeline(response.body, createWriteStream(destination));
57+
58+
return;
59+
} catch (cause) {
60+
lastError = cause;
61+
62+
rmSync(destination, { force: true });
63+
64+
if (
65+
!isRetryableDownloadError(cause) ||
66+
attempt >= DOWNLOAD_BACKOFFS.length
67+
) {
68+
break;
69+
}
70+
71+
await delay(DOWNLOAD_BACKOFFS[attempt]);
72+
}
3173
}
74+
75+
throw new Error(`${ERR_INSTALL_FAILED_TO_DOWNLOAD_ASSET}: ${asset.asset}`, {
76+
cause: lastError,
77+
});
3278
}

0 commit comments

Comments
 (0)