diff options
| author | Tommaso Sciortino <[email protected]> | 2025-05-30 18:25:47 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-05-30 18:25:47 -0700 |
| commit | 21fba832d1b4ea7af43fb887d9b2b38fcf8210d0 (patch) | |
| tree | 7200d2fac3a55c385e0a2dac34b5282c942364bc /packages/server/src/utils/retry.ts | |
| parent | c81148a0cc8489f657901c2cc7247c0834075e1a (diff) | |
Rename server->core (#638)
Diffstat (limited to 'packages/server/src/utils/retry.ts')
| -rw-r--r-- | packages/server/src/utils/retry.ts | 227 |
1 files changed, 0 insertions, 227 deletions
diff --git a/packages/server/src/utils/retry.ts b/packages/server/src/utils/retry.ts deleted file mode 100644 index 1e7d5bcb..00000000 --- a/packages/server/src/utils/retry.ts +++ /dev/null @@ -1,227 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -export interface RetryOptions { - maxAttempts: number; - initialDelayMs: number; - maxDelayMs: number; - shouldRetry: (error: Error) => boolean; -} - -const DEFAULT_RETRY_OPTIONS: RetryOptions = { - maxAttempts: 5, - initialDelayMs: 5000, - maxDelayMs: 30000, // 30 seconds - shouldRetry: defaultShouldRetry, -}; - -/** - * Default predicate function to determine if a retry should be attempted. - * Retries on 429 (Too Many Requests) and 5xx server errors. - * @param error The error object. - * @returns True if the error is a transient error, false otherwise. - */ -function defaultShouldRetry(error: Error | unknown): boolean { - // Check for common transient error status codes either in message or a status property - if (error && typeof (error as { status?: number }).status === 'number') { - const status = (error as { status: number }).status; - if (status === 429 || (status >= 500 && status < 600)) { - return true; - } - } - if (error instanceof Error && error.message) { - if (error.message.includes('429')) return true; - if (error.message.match(/5\d{2}/)) return true; - } - return false; -} - -/** - * Delays execution for a specified number of milliseconds. - * @param ms The number of milliseconds to delay. - * @returns A promise that resolves after the delay. - */ -function delay(ms: number): Promise<void> { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -/** - * Retries a function with exponential backoff and jitter. - * @param fn The asynchronous function to retry. - * @param options Optional retry configuration. - * @returns A promise that resolves with the result of the function if successful. - * @throws The last error encountered if all attempts fail. - */ -export async function retryWithBackoff<T>( - fn: () => Promise<T>, - options?: Partial<RetryOptions>, -): Promise<T> { - const { maxAttempts, initialDelayMs, maxDelayMs, shouldRetry } = { - ...DEFAULT_RETRY_OPTIONS, - ...options, - }; - - let attempt = 0; - let currentDelay = initialDelayMs; - - while (attempt < maxAttempts) { - attempt++; - try { - return await fn(); - } catch (error) { - if (attempt >= maxAttempts || !shouldRetry(error as Error)) { - throw error; - } - - const { delayDurationMs, errorStatus } = getDelayDurationAndStatus(error); - - if (delayDurationMs > 0) { - // Respect Retry-After header if present and parsed - console.warn( - `Attempt ${attempt} failed with status ${errorStatus ?? 'unknown'}. Retrying after explicit delay of ${delayDurationMs}ms...`, - error, - ); - await delay(delayDurationMs); - // Reset currentDelay for next potential non-429 error, or if Retry-After is not present next time - currentDelay = initialDelayMs; - } else { - // Fallback to exponential backoff with jitter - logRetryAttempt(attempt, error, errorStatus); - // Add jitter: +/- 30% of currentDelay - const jitter = currentDelay * 0.3 * (Math.random() * 2 - 1); - const delayWithJitter = Math.max(0, currentDelay + jitter); - await delay(delayWithJitter); - currentDelay = Math.min(maxDelayMs, currentDelay * 2); - } - } - } - // This line should theoretically be unreachable due to the throw in the catch block. - // Added for type safety and to satisfy the compiler that a promise is always returned. - throw new Error('Retry attempts exhausted'); -} - -/** - * Extracts the HTTP status code from an error object. - * @param error The error object. - * @returns The HTTP status code, or undefined if not found. - */ -function getErrorStatus(error: unknown): number | undefined { - if (typeof error === 'object' && error !== null) { - if ('status' in error && typeof error.status === 'number') { - return error.status; - } - // Check for error.response.status (common in axios errors) - if ( - 'response' in error && - typeof (error as { response?: unknown }).response === 'object' && - (error as { response?: unknown }).response !== null - ) { - const response = ( - error as { response: { status?: unknown; headers?: unknown } } - ).response; - if ('status' in response && typeof response.status === 'number') { - return response.status; - } - } - } - return undefined; -} - -/** - * Extracts the Retry-After delay from an error object's headers. - * @param error The error object. - * @returns The delay in milliseconds, or 0 if not found or invalid. - */ -function getRetryAfterDelayMs(error: unknown): number { - if (typeof error === 'object' && error !== null) { - // Check for error.response.headers (common in axios errors) - if ( - 'response' in error && - typeof (error as { response?: unknown }).response === 'object' && - (error as { response?: unknown }).response !== null - ) { - const response = (error as { response: { headers?: unknown } }).response; - if ( - 'headers' in response && - typeof response.headers === 'object' && - response.headers !== null - ) { - const headers = response.headers as { 'retry-after'?: unknown }; - const retryAfterHeader = headers['retry-after']; - if (typeof retryAfterHeader === 'string') { - const retryAfterSeconds = parseInt(retryAfterHeader, 10); - if (!isNaN(retryAfterSeconds)) { - return retryAfterSeconds * 1000; - } - // It might be an HTTP date - const retryAfterDate = new Date(retryAfterHeader); - if (!isNaN(retryAfterDate.getTime())) { - return Math.max(0, retryAfterDate.getTime() - Date.now()); - } - } - } - } - } - return 0; -} - -/** - * Determines the delay duration based on the error, prioritizing Retry-After header. - * @param error The error object. - * @returns An object containing the delay duration in milliseconds and the error status. - */ -function getDelayDurationAndStatus(error: unknown): { - delayDurationMs: number; - errorStatus: number | undefined; -} { - const errorStatus = getErrorStatus(error); - let delayDurationMs = 0; - - if (errorStatus === 429) { - delayDurationMs = getRetryAfterDelayMs(error); - } - return { delayDurationMs, errorStatus }; -} - -/** - * Logs a message for a retry attempt when using exponential backoff. - * @param attempt The current attempt number. - * @param error The error that caused the retry. - * @param errorStatus The HTTP status code of the error, if available. - */ -function logRetryAttempt( - attempt: number, - error: unknown, - errorStatus?: number, -): void { - let message = `Attempt ${attempt} failed. Retrying with backoff...`; - if (errorStatus) { - message = `Attempt ${attempt} failed with status ${errorStatus}. Retrying with backoff...`; - } - - if (errorStatus === 429) { - console.warn(message, error); - } else if (errorStatus && errorStatus >= 500 && errorStatus < 600) { - console.error(message, error); - } else if (error instanceof Error) { - // Fallback for errors that might not have a status but have a message - if (error.message.includes('429')) { - console.warn( - `Attempt ${attempt} failed with 429 error (no Retry-After header). Retrying with backoff...`, - error, - ); - } else if (error.message.match(/5\d{2}/)) { - console.error( - `Attempt ${attempt} failed with 5xx error. Retrying with backoff...`, - error, - ); - } else { - console.warn(message, error); // Default to warn for other errors - } - } else { - console.warn(message, error); // Default to warn if error type is unknown - } -} |
