From a4bcacfc9da3582ae005c6a39fbb01dfc5621e41 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Wed, 4 Sep 2024 15:15:58 +0000 Subject: [PATCH] [PAPI] Retry steams upon not receiving data for 10 seconds --- .../dashboard/src/service/public-api.ts | 18 +++--- .../gitpod-protocol/src/util/timeout.ts | 55 +++++++++++++++++++ 2 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 components/gitpod-protocol/src/util/timeout.ts diff --git a/components/dashboard/src/service/public-api.ts b/components/dashboard/src/service/public-api.ts index 9c6af952392ef9..43499c70883746 100644 --- a/components/dashboard/src/service/public-api.ts +++ b/components/dashboard/src/service/public-api.ts @@ -41,6 +41,7 @@ import { VerificationService } from "@gitpod/public-api/lib/gitpod/v1/verificati import { JsonRpcInstallationClient } from "./json-rpc-installation-client"; import { InstallationService } from "@gitpod/public-api/lib/gitpod/v1/installation_connect"; import { JsonRpcUserClient } from "./json-rpc-user-client"; +import { Timeout } from "@gitpod/gitpod-protocol/lib/util/timeout"; const transport = createConnectTransport({ baseUrl: `${window.location.protocol}//${window.location.host}/public-api`, @@ -274,24 +275,17 @@ export function stream( const abort = new AbortController(); (async () => { while (!abort.signal.aborted) { + const connectionTimeout = new Timeout(10_000); try { - let dataReceived = false; - let attemptTimeoutId: NodeJS.Timeout | null; - attemptTimeoutId = setTimeout(() => { - if (!dataReceived) { - throw new Error("Attempt Timeout: No initial data received within 10 seconds"); - } - }, 10_000); + connectionTimeout.start(); for await (const response of factory({ - signal: abort.signal, + signal: AbortSignal.any([abort.signal, connectionTimeout.signal()!]), // GCP timeout is 10 minutes, we timeout 3 mins earlier // to avoid unknown network errors and reconnect gracefully timeoutMs: 7 * 60 * 1000, })) { - dataReceived = true; - attemptTimeoutId && clearTimeout(attemptTimeoutId); - attemptTimeoutId = null; + connectionTimeout.clear(); // connection is alive now, clear timeout backoff = BASE_BACKOFF; cb(response); @@ -314,6 +308,8 @@ export function stream( backoff = Math.min(2 * backoff, MAX_BACKOFF); console.error(e); } + } finally { + connectionTimeout.clear(); } const jitter = Math.random() * 0.3 * backoff; const delay = backoff + jitter; diff --git a/components/gitpod-protocol/src/util/timeout.ts b/components/gitpod-protocol/src/util/timeout.ts new file mode 100644 index 00000000000000..3e354004fcff3c --- /dev/null +++ b/components/gitpod-protocol/src/util/timeout.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +/** + * A resettable timeout, based on an AbortController + setTimeout. + */ +export class Timeout { + private _timer: NodeJS.Timeout | undefined; + private _abortController: AbortController | undefined; + + constructor(readonly timeout: number, readonly abortCondition?: () => boolean) {} + + /** + * Starts a new timeout (and clears the old one). Identical to `reset`. + */ + public start() { + this.reset(); + } + + /** + * Starts a new timeout (and clears the old one). + */ + public reset() { + this.clear(); + + const abortController = new AbortController(); + this._abortController = abortController; + this._timer = setTimeout(() => { + if (this.abortCondition && this.abortCondition()) { + return; + } + + abortController.abort(); + }, this.timeout); + } + + public clear() { + if (this._timer) { + clearTimeout(this._timer); + this._timer = undefined; + } + if (this._abortController) { + this._abortController = undefined; + } + } + + public signal(): AbortSignal | undefined { + return this._abortController?.signal; + } +} + +export class CombinedAbortSignal extends AbortSignal {}