From e1958c1f45c444972716e9bcc3c53309f52232d4 Mon Sep 17 00:00:00 2001 From: Joaquin Colacci Date: Thu, 7 Dec 2023 13:19:29 -0500 Subject: [PATCH] test: fetch retry (#162) This PR adds retry to the fetch requests. It also fixes the tests that were having issues when running locally. --- src/clients/admin.ts | 6 ++--- src/clients/cloud.ts | 4 ++-- src/clients/lsp.ts | 8 +++---- src/providers/auth.ts | 3 +-- src/providers/results.ts | 3 +-- src/test/suite/extension.test.ts | 5 ++-- src/test/suite/server.ts | 4 ++-- src/utilities/error.ts | 6 ++++- src/utilities/getNonce.ts | 8 ------- src/utilities/getUri.ts | 5 ---- src/utilities/utils.ts | 41 ++++++++++++++++++++++++++++++++ 11 files changed, 62 insertions(+), 31 deletions(-) delete mode 100644 src/utilities/getNonce.ts delete mode 100644 src/utilities/getUri.ts create mode 100644 src/utilities/utils.ts diff --git a/src/clients/admin.ts b/src/clients/admin.ts index 1e932e0..b457e0b 100644 --- a/src/clients/admin.ts +++ b/src/clients/admin.ts @@ -1,8 +1,8 @@ -import fetch from "node-fetch"; import AppPassword from "../context/appPassword"; import { Errors, ExtensionError } from "../utilities/error"; import jwksClient from "jwks-rsa"; import { verify } from "node-jsonwebtoken"; +import { fetchWithRetry } from "../utilities/utils"; interface AuthenticationResponse { accessToken: string, @@ -44,11 +44,11 @@ export default class AdminClient { secret: this.appPassword.secretKey }; - const response = await fetch(this.adminEndpoint, { + const response = await fetchWithRetry(this.adminEndpoint, { method: 'post', body: JSON.stringify(authRequest), // eslint-disable-next-line @typescript-eslint/naming-convention - headers: {'Content-Type': 'application/json'} + headers: {'Content-Type': 'application/json' } }); if (response.status === 200) { diff --git a/src/clients/cloud.ts b/src/clients/cloud.ts index 253ea0b..4a46822 100644 --- a/src/clients/cloud.ts +++ b/src/clients/cloud.ts @@ -1,6 +1,6 @@ -import fetch from "node-fetch"; import AdminClient from "./admin"; import { Errors, ExtensionError } from "../utilities/error"; +import { fetchWithRetry } from "../utilities/utils"; const DEFAULT_API_CLOUD_ENDPOINT = 'https://api.cloud.materialize.com'; @@ -58,7 +58,7 @@ export default class CloudClient { } async fetch(endpoint: string) { - return fetch(endpoint, { + return fetchWithRetry(endpoint, { method: 'get', headers: { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/src/clients/lsp.ts b/src/clients/lsp.ts index 545ffd6..ae1d6e8 100644 --- a/src/clients/lsp.ts +++ b/src/clients/lsp.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import fetch from "node-fetch"; import path from "path"; import * as vscode from "vscode"; import { @@ -16,6 +15,7 @@ import os from "os"; import { SemVer } from "semver"; import { Errors, ExtensionError } from "../utilities/error"; import * as Sentry from "@sentry/node"; +import { fetchWithRetry } from "../utilities/utils"; // This endpoint returns a string with the latest LSP version. const BINARIES_ENDPOINT = "https://binaries.materialize.com"; @@ -144,7 +144,7 @@ export default class LspClient { bufferStream .pipe(gunzip) .pipe(extract) - .on('finish', (d: any) => { + .on('finish', () => { console.log("[LSP]", "Server installed."); res(""); }) @@ -198,7 +198,7 @@ export default class LspClient { */ private async fetchLatestVersionNumber() { console.log("[LSP]", "Fetching latest version number."); - const response = await fetch(LATEST_VERSION_ENDPOINT); + const response = await fetchWithRetry(LATEST_VERSION_ENDPOINT); const latestVersion: string = await response.text(); return new SemVer(latestVersion); @@ -213,7 +213,7 @@ export default class LspClient { const endpoint = this.getEndpointByOs(latestVersion); console.log("[LSP]", `Fetching LSP from: ${endpoint}`); - const binaryResponse = await fetch(endpoint); + const binaryResponse = await fetchWithRetry(endpoint); const buffer = await binaryResponse.arrayBuffer(); return buffer; diff --git a/src/providers/auth.ts b/src/providers/auth.ts index a50ad3b..6110580 100644 --- a/src/providers/auth.ts +++ b/src/providers/auth.ts @@ -1,8 +1,7 @@ import * as vscode from "vscode"; import express, { Request, Response, Application, } from 'express'; -import { getUri } from "../utilities/getUri"; +import { getNonce, getUri } from "../utilities/utils"; import AppPassword from "../context/appPassword"; -import { getNonce } from "../utilities/getNonce"; import AsyncContext from "../context/asyncContext"; import { Errors, ExtensionError } from "../utilities/error"; diff --git a/src/providers/results.ts b/src/providers/results.ts index 924cc4e..88187c7 100644 --- a/src/providers/results.ts +++ b/src/providers/results.ts @@ -1,6 +1,5 @@ import * as vscode from "vscode"; -import { getUri } from "../utilities/getUri"; -import { getNonce } from "../utilities/getNonce"; +import { getNonce, getUri } from "../utilities/utils"; import { QueryResult } from "pg"; interface Results extends QueryResult { diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index bc4214d..ccb1312 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -255,13 +255,14 @@ suite('Extension Test Suite', () => { let err = false; try { await context.setProfile("invalid_profile"); - } catch (error) { + } catch (error: any) { + assert.ok(error.message === "Invalid authentication"); err = true; } assert.ok(err); - }).timeout(10000); + }).timeout(30000); test('Alternative parser', async () => { const lsp = new LspClient(); diff --git a/src/test/suite/server.ts b/src/test/suite/server.ts index ea6a36f..30d524d 100644 --- a/src/test/suite/server.ts +++ b/src/test/suite/server.ts @@ -80,8 +80,8 @@ export function mockServer(): Promise { }); return new Promise((res) => { - app.listen(3000, 'localhost', () => { - console.log(`Mock server listening at localhost:3000`); + const server = app.listen(3000, 'localhost', () => { + console.log(`Mock server listening at localhost:3000: `, server.listening); res("Loaded."); }); }); diff --git a/src/utilities/error.ts b/src/utilities/error.ts index 89a4851..e403754 100644 --- a/src/utilities/error.ts +++ b/src/utilities/error.ts @@ -149,5 +149,9 @@ export enum Errors { /** * Raises when it is impossible to parse the statements. */ - parsingFailure = " Error parsing the statements.", + parsingFailure = "Error parsing the statements.", + /** + * Raises when a fetch failes after a minute. + */ + fetchTimeoutError = "Failed to fetch after a minute." } diff --git a/src/utilities/getNonce.ts b/src/utilities/getNonce.ts deleted file mode 100644 index 205ef23..0000000 --- a/src/utilities/getNonce.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function getNonce() { - let text = ""; - const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; - } \ No newline at end of file diff --git a/src/utilities/getUri.ts b/src/utilities/getUri.ts deleted file mode 100644 index f43ed04..0000000 --- a/src/utilities/getUri.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Uri, Webview } from "vscode"; - -export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { - return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); -} \ No newline at end of file diff --git a/src/utilities/utils.ts b/src/utilities/utils.ts new file mode 100644 index 0000000..a582a0c --- /dev/null +++ b/src/utilities/utils.ts @@ -0,0 +1,41 @@ +import { Uri, Webview } from "vscode"; +import { Errors, ExtensionError } from "./error"; + +export function getNonce() { + let text = ""; + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + +export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { + return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); +} + +export async function fetchWithRetry( + url: string, + init?: RequestInit, + timeout = 60000, + interval = 5000 +): Promise { + const startTime = Date.now(); + + async function attemptFetch(): Promise { + try { + const response = await fetch(url, init); + return response; + } catch (error) { + console.error("[Fetch]", "Error fetching: ", error); + if (Date.now() - startTime < timeout) { + await new Promise(resolve => setTimeout(resolve, interval)); + return attemptFetch(); + } else { + throw new ExtensionError(Errors.fetchTimeoutError, `Failed to fetch after ${timeout}ms: ${error}`); + } + } + } + + return attemptFetch(); +} \ No newline at end of file