Skip to content

Commit f18a0de

Browse files
committed
Extract timeout and apply to it to CLI downloads
1 parent feeb9d6 commit f18a0de

4 files changed

Lines changed: 104 additions & 33 deletions

File tree

extensions/ql-vscode/src/codeql-cli/distribution.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { WriteStream } from "fs";
12
import { createWriteStream, mkdtemp, pathExists, remove } from "fs-extra";
23
import { tmpdir } from "os";
34
import { delimiter, dirname, join } from "path";
@@ -26,6 +27,8 @@ import { unzipToDirectoryConcurrently } from "../common/unzip-concurrently";
2627
import { reportUnzipProgress } from "../common/vscode/unzip-progress";
2728
import type { Release } from "./distribution/release";
2829
import { ReleasesApiConsumer } from "./distribution/releases-api-consumer";
30+
import { createTimeoutSignal } from "../common/fetch-stream";
31+
import { AbortError } from "node-fetch";
2932

3033
/**
3134
* distribution.ts
@@ -384,15 +387,21 @@ class ExtensionSpecificDistributionManager {
384387
);
385388
}
386389

387-
const assetStream =
388-
await this.createReleasesApiConsumer().streamBinaryContentOfAsset(
389-
assets[0],
390-
);
390+
const { signal, onData, dispose: disposeTimeout } = createTimeoutSignal(30);
391+
391392
const tmpDirectory = await mkdtemp(join(tmpdir(), "vscode-codeql"));
392393

394+
let archiveFile: WriteStream | undefined = undefined;
395+
393396
try {
397+
const assetStream =
398+
await this.createReleasesApiConsumer().streamBinaryContentOfAsset(
399+
assets[0],
400+
signal,
401+
);
402+
394403
const archivePath = join(tmpDirectory, "distributionDownload.zip");
395-
const archiveFile = createWriteStream(archivePath);
404+
archiveFile = createWriteStream(archivePath);
396405

397406
const contentLength = assetStream.headers.get("content-length");
398407
const totalNumBytes = contentLength
@@ -405,12 +414,23 @@ class ExtensionSpecificDistributionManager {
405414
progressCallback,
406415
);
407416

408-
await new Promise((resolve, reject) =>
417+
assetStream.body.on("data", onData);
418+
419+
await new Promise((resolve, reject) => {
420+
if (!archiveFile) {
421+
throw new Error("Invariant violation: archiveFile not set");
422+
}
423+
409424
assetStream.body
410425
.pipe(archiveFile)
411426
.on("finish", resolve)
412-
.on("error", reject),
413-
);
427+
.on("error", reject);
428+
429+
// If an error occurs on the body, we also want to reject the promise (e.g. during a timeout error).
430+
assetStream.body.on("error", reject);
431+
});
432+
433+
disposeTimeout();
414434

415435
await this.bumpDistributionFolderIndex();
416436

@@ -427,7 +447,19 @@ class ExtensionSpecificDistributionManager {
427447
)
428448
: undefined,
429449
);
450+
} catch (e) {
451+
if (e instanceof AbortError) {
452+
const thrownError = new AbortError("The download timed out.");
453+
thrownError.stack = e.stack;
454+
throw thrownError;
455+
}
456+
457+
throw e;
430458
} finally {
459+
disposeTimeout();
460+
461+
archiveFile?.close();
462+
431463
await remove(tmpDirectory);
432464
}
433465
}

extensions/ql-vscode/src/codeql-cli/distribution/releases-api-consumer.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,21 +90,28 @@ export class ReleasesApiConsumer {
9090

9191
public async streamBinaryContentOfAsset(
9292
asset: ReleaseAsset,
93+
signal?: AbortSignal,
9394
): Promise<Response> {
9495
const apiPath = `/repos/${this.repositoryNwo}/releases/assets/${asset.id}`;
9596

96-
return await this.makeApiCall(apiPath, {
97-
accept: "application/octet-stream",
98-
});
97+
return await this.makeApiCall(
98+
apiPath,
99+
{
100+
accept: "application/octet-stream",
101+
},
102+
signal,
103+
);
99104
}
100105

101106
protected async makeApiCall(
102107
apiPath: string,
103108
additionalHeaders: { [key: string]: string } = {},
109+
signal?: AbortSignal,
104110
): Promise<Response> {
105111
const response = await this.makeRawRequest(
106112
ReleasesApiConsumer.apiBase + apiPath,
107113
Object.assign({}, this.defaultHeaders, additionalHeaders),
114+
signal,
108115
);
109116

110117
if (!response.ok) {
@@ -129,11 +136,13 @@ export class ReleasesApiConsumer {
129136
private async makeRawRequest(
130137
requestUrl: string,
131138
headers: { [key: string]: string },
139+
signal?: AbortSignal,
132140
redirectCount = 0,
133141
): Promise<Response> {
134142
const response = await fetch(requestUrl, {
135143
headers,
136144
redirect: "manual",
145+
signal,
137146
});
138147

139148
const redirectUrl = response.headers.get("location");
@@ -153,7 +162,12 @@ export class ReleasesApiConsumer {
153162
// mechanism is provided.
154163
delete headers["authorization"];
155164
}
156-
return await this.makeRawRequest(redirectUrl, headers, redirectCount + 1);
165+
return await this.makeRawRequest(
166+
redirectUrl,
167+
headers,
168+
signal,
169+
redirectCount + 1,
170+
);
157171
}
158172

159173
return response;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { clearTimeout } from "node:timers";
2+
3+
export function createTimeoutSignal(timeoutSeconds: number): {
4+
signal: AbortSignal;
5+
onData: () => void;
6+
dispose: () => void;
7+
} {
8+
const timeout = timeoutSeconds * 1000;
9+
10+
const abortController = new AbortController();
11+
12+
let timeoutId: NodeJS.Timeout;
13+
14+
// If we don't get any data within the timeout, abort the download
15+
timeoutId = setTimeout(() => {
16+
abortController.abort();
17+
}, timeout);
18+
19+
// If we receive any data within the timeout, reset the timeout
20+
const onData = () => {
21+
clearTimeout(timeoutId);
22+
timeoutId = setTimeout(() => {
23+
abortController.abort();
24+
}, timeout);
25+
};
26+
27+
const dispose = () => {
28+
clearTimeout(timeoutId);
29+
};
30+
31+
return {
32+
signal: abortController.signal,
33+
onData,
34+
dispose,
35+
};
36+
}

extensions/ql-vscode/src/databases/database-fetcher.ts

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { showAndLogInformationMessage } from "../common/logging";
3737
import { AppOctokit } from "../common/octokit";
3838
import { getLanguageDisplayName } from "../common/query-language";
3939
import type { DatabaseOrigin } from "./local-databases/database-origin";
40-
import { clearTimeout } from "node:timers";
40+
import { createTimeoutSignal } from "../common/fetch-stream";
4141

4242
/**
4343
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
@@ -483,28 +483,23 @@ async function fetchAndUnzip(
483483
step: 1,
484484
});
485485

486-
const abortController = new AbortController();
487-
488-
const timeout = downloadTimeout() * 1000;
489-
490-
let timeoutId: NodeJS.Timeout;
491-
492-
// If we don't get any data within the timeout, abort the download
493-
timeoutId = setTimeout(() => {
494-
abortController.abort();
495-
}, timeout);
486+
const {
487+
signal,
488+
onData,
489+
dispose: disposeTimeout,
490+
} = createTimeoutSignal(downloadTimeout());
496491

497492
let response: Response;
498493
try {
499494
response = await checkForFailingResponse(
500495
await fetch(databaseUrl, {
501496
headers: requestHeaders,
502-
signal: abortController.signal,
497+
signal,
503498
}),
504499
"Error downloading database",
505500
);
506501
} catch (e) {
507-
clearTimeout(timeoutId);
502+
disposeTimeout();
508503

509504
if (e instanceof AbortError) {
510505
const thrownError = new AbortError("The request timed out.");
@@ -526,13 +521,7 @@ async function fetchAndUnzip(
526521
progress,
527522
);
528523

529-
// If we receive any data within the timeout, reset the timeout
530-
response.body.on("data", () => {
531-
clearTimeout(timeoutId);
532-
timeoutId = setTimeout(() => {
533-
abortController.abort();
534-
}, timeout);
535-
});
524+
response.body.on("data", onData);
536525

537526
try {
538527
await new Promise((resolve, reject) => {
@@ -558,7 +547,7 @@ async function fetchAndUnzip(
558547

559548
throw e;
560549
} finally {
561-
clearTimeout(timeoutId);
550+
disposeTimeout();
562551
}
563552

564553
await readAndUnzip(

0 commit comments

Comments
 (0)