Skip to content

Commit 9511163

Browse files
committed
push: Implement retry logic (test)
1 parent b28155e commit 9511163

File tree

2 files changed

+55
-18
lines changed

2 files changed

+55
-18
lines changed

push/index.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { $, CryptoHasher, file, write } from "bun";
1+
import { $, CryptoHasher, file, sleep, write } from "bun";
22
import { extract } from "tar";
33

44
import stream from "node:stream";
@@ -71,7 +71,7 @@ if (!(await file(tarFile).exists())) {
7171

7272
await mkdir(imagePath);
7373

74-
const result = await extract({
74+
await extract({
7575
file: tarFile,
7676
cwd: imagePath,
7777
});
@@ -251,7 +251,6 @@ async function pushLayer(layerDigest: string, readableStream: ReadableStream, to
251251
throw new Error(`oci-chunk-max-length header is malformed (not a number)`);
252252
}
253253

254-
const reader = readableStream.getReader();
255254
const uploadId = createUploadResponse.headers.get("docker-upload-uuid");
256255
if (uploadId === null) {
257256
throw new Error("Docker-Upload-UUID not defined in headers");
@@ -271,9 +270,13 @@ async function pushLayer(layerDigest: string, readableStream: ReadableStream, to
271270
let written = 0;
272271
let previousReadable: ReadableLimiter | undefined;
273272
let totalLayerSizeLeft = totalLayerSize;
273+
const reader = readableStream.getReader();
274+
let fail = "true";
275+
let failures = 0;
274276
while (totalLayerSizeLeft > 0) {
275277
const range = `0-${Math.min(end, totalLayerSize) - 1}`;
276278
const current = new ReadableLimiter(reader as ReadableStreamDefaultReader, maxToWrite, previousReadable);
279+
await current.init();
277280
const patchChunkUploadURL = parseLocation(location);
278281
// we have to do fetchNode because Bun doesn't allow setting custom Content-Length.
279282
// https://github.com/oven-sh/bun/issues/10507
@@ -284,14 +287,19 @@ async function pushLayer(layerDigest: string, readableStream: ReadableStream, to
284287
"range": range,
285288
"authorization": cred,
286289
"content-length": `${Math.min(totalLayerSizeLeft, maxToWrite)}`,
290+
"x-fail": fail,
287291
}),
288292
});
289293
if (!patchChunkResult.ok) {
290-
throw new Error(
291-
`uploading chunk ${patchChunkUploadURL} returned ${patchChunkResult.status}: ${await patchChunkResult.text()}`,
292-
);
294+
previousReadable = current;
295+
console.error(`${layerDigest}: Pushing ${range} failed with ${patchChunkResult.status}, retrying`);
296+
await sleep(500);
297+
if (failures++ >= 2) fail = "false";
298+
continue;
293299
}
294300

301+
fail = "true";
302+
current.ok();
295303
const rangeResponse = patchChunkResult.headers.get("range");
296304
if (rangeResponse !== range) {
297305
throw new Error(`unexpected Range header ${rangeResponse}, expected ${range}`);
@@ -308,18 +316,24 @@ async function pushLayer(layerDigest: string, readableStream: ReadableStream, to
308316
const range = `0-${written - 1}`;
309317
const uploadURL = new URL(parseLocation(location));
310318
uploadURL.searchParams.append("digest", layerDigest);
311-
const response = await fetch(uploadURL.toString(), {
312-
method: "PUT",
313-
headers: new Headers({
314-
Range: range,
315-
Authorization: cred,
316-
}),
317-
});
318-
if (!response.ok) {
319-
throw new Error(`${uploadURL.toString()} failed with ${response.status}: ${await response.text()}`);
319+
for (let tries = 0; tries < 3; tries++) {
320+
const response = await fetch(uploadURL.toString(), {
321+
method: "PUT",
322+
headers: new Headers({
323+
Range: range,
324+
Authorization: cred,
325+
}),
326+
});
327+
if (!response.ok) {
328+
console.error(`${layerDigest}: Finishing ${range} failed with ${response.status}, retrying`);
329+
continue;
330+
}
331+
332+
console.log("Pushed", layerDigest);
333+
return;
320334
}
321335

322-
console.log("Pushed", layerDigest);
336+
throw new Error(`Could not push after multiple tries`);
323337
}
324338

325339
const layersManifest = [] as {

push/limiter.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import stream from "node:stream";
66
export class ReadableLimiter extends stream.Readable {
77
public written: number = 0;
88
private leftover: Uint8Array | undefined;
9+
private promise: Promise<Uint8Array> | undefined;
10+
private accumulator: Uint8Array[];
911

1012
constructor(
1113
// reader will be used to read bytes until limit.
@@ -17,7 +19,27 @@ export class ReadableLimiter extends stream.Readable {
1719
) {
1820
super();
1921

20-
if (previousReader) this.leftover = previousReader.leftover;
22+
if (previousReader) {
23+
this.leftover = previousReader.leftover;
24+
if (previousReader.accumulator.length > 0) {
25+
this.promise = new Blob(previousReader.accumulator).bytes();
26+
previousReader.accumulator = [];
27+
}
28+
}
29+
30+
this.accumulator = [];
31+
}
32+
33+
async init() {
34+
if (this.promise !== undefined) {
35+
if (this.leftover !== undefined && this.leftover.length > 0)
36+
this.leftover = await new Blob([await this.promise, this.leftover ?? []]).bytes();
37+
else this.leftover = await this.promise;
38+
}
39+
}
40+
41+
ok() {
42+
this.accumulator = [];
2143
}
2244

2345
_read(): void {
@@ -27,6 +49,7 @@ export class ReadableLimiter extends stream.Readable {
2749

2850
if (this.leftover !== undefined) {
2951
const toPushNow = this.leftover.slice(0, this.limit);
52+
this.accumulator.push(toPushNow);
3053
this.leftover = this.leftover.slice(this.limit);
3154
this.push(toPushNow);
3255
this.limit -= toPushNow.length;
@@ -50,7 +73,7 @@ export class ReadableLimiter extends stream.Readable {
5073
}
5174

5275
if (arr.length === 0) return this.push(null);
53-
76+
this.accumulator.push(arr);
5477
this.push(arr);
5578
this.limit -= arr.length;
5679
this.written += arr.length;

0 commit comments

Comments
 (0)