diff --git a/src/internal/client.ts b/src/internal/client.ts index 3b4520d1..a4d81ad7 100644 --- a/src/internal/client.ts +++ b/src/internal/client.ts @@ -155,6 +155,29 @@ const requestOptionProperties = [ 'sessionIdContext', ] as const +export interface RetryOptions { + /** + * If this set to true, it will take precedence over all other retry options. + * @default false + */ + disableRetry?: boolean + /** + * The maximum amount of retries for a request. + * @default 1 + */ + maximumRetryCount?: number + /** + * The minimum duration (in milliseconds) for the exponential backoff algorithm. + * @default 100 + */ + baseDelayMs?: number + /** + * The maximum duration (in milliseconds) for the exponential backoff algorithm. + * @default 60000 + */ + maximumDelayMs?: number +} + export interface ClientOptions { endPoint: string accessKey?: string @@ -169,6 +192,7 @@ export interface ClientOptions { credentialsProvider?: CredentialProvider s3AccelerateEndpoint?: string transportAgent?: http.Agent + retryOptions?: RetryOptions } export type RequestOption = Partial & { @@ -212,6 +236,7 @@ export class TypedClient { protected credentialsProvider?: CredentialProvider partSize: number = 64 * 1024 * 1024 protected overRidePartSize?: boolean + protected retryOptions: RetryOptions protected maximumPartSize = 5 * 1024 * 1024 * 1024 protected maxObjectSize = 5 * 1024 * 1024 * 1024 * 1024 @@ -352,6 +377,20 @@ export class TypedClient { this.s3AccelerateEndpoint = params.s3AccelerateEndpoint || undefined this.reqOptions = {} this.clientExtensions = new Extensions(this) + + if (params.retryOptions) { + if (!isObject(params.retryOptions)) { + throw new errors.InvalidArgumentError( + `Invalid retryOptions type: ${params.retryOptions}, expected to be type "object"`, + ) + } + + this.retryOptions = params.retryOptions + } else { + this.retryOptions = { + disableRetry: false, + } + } } /** * Minio extensions that aren't necessary present for Amazon S3 compatible storage servers @@ -724,7 +763,14 @@ export class TypedClient { reqOptions.headers.authorization = signV4(reqOptions, this.accessKey, this.secretKey, region, date, sha256sum) } - const response = await requestWithRetry(this.transport, reqOptions, body) + const response = await requestWithRetry( + this.transport, + reqOptions, + body, + this.retryOptions.disableRetry === true ? 0 : this.retryOptions.maximumRetryCount, + this.retryOptions.baseDelayMs, + this.retryOptions.maximumDelayMs, + ) if (!response.statusCode) { throw new Error("BUG: response doesn't have a statusCode") } diff --git a/src/internal/request.ts b/src/internal/request.ts index 25b9af47..0d8bce6c 100644 --- a/src/internal/request.ts +++ b/src/internal/request.ts @@ -28,9 +28,9 @@ export async function request( }) } -const MAX_RETRIES = 10 -const EXP_BACK_OFF_BASE_DELAY = 1000 // Base delay for exponential backoff -const ADDITIONAL_DELAY_FACTOR = 1.0 // to avoid synchronized retries +const MAX_RETRIES = 1 +const BASE_DELAY_MS = 100 // Base delay for exponential backoff +const MAX_DELAY_MS = 60000 // Max delay for exponential backoff // Retryable error codes for HTTP ( ref: minio-go) export const retryHttpCodes: Record = { @@ -52,10 +52,10 @@ const sleep = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)) } -const getExpBackOffDelay = (retryCount: number) => { - const backOffBy = EXP_BACK_OFF_BASE_DELAY * 2 ** retryCount - const additionalDelay = Math.random() * backOffBy * ADDITIONAL_DELAY_FACTOR - return backOffBy + additionalDelay +const getExpBackOffDelay = (retryCount: number, baseDelayMs: number, maximumDelayMs: number) => { + const backOffBy = baseDelayMs * (1 << retryCount) + const additionalDelay = Math.random() * backOffBy + return Math.min(backOffBy + additionalDelay, maximumDelayMs) } export async function requestWithRetry( @@ -63,6 +63,8 @@ export async function requestWithRetry( opt: https.RequestOptions, body: Buffer | string | stream.Readable | null = null, maxRetries: number = MAX_RETRIES, + baseDelayMs: number = BASE_DELAY_MS, + maximumDelayMs: number = MAX_DELAY_MS, ): Promise { let attempt = 0 let isRetryable = false @@ -76,7 +78,7 @@ export async function requestWithRetry( } return response // Success, return the raw response - } catch (err) { + } catch (err: unknown) { if (isRetryable) { attempt++ isRetryable = false @@ -84,7 +86,7 @@ export async function requestWithRetry( if (attempt > maxRetries) { throw new Error(`Request failed after ${maxRetries} retries: ${err}`) } - const delay = getExpBackOffDelay(attempt) + const delay = getExpBackOffDelay(attempt, baseDelayMs, maximumDelayMs) // eslint-disable-next-line no-console console.warn( `${new Date().toLocaleString()} Retrying request (attempt ${attempt}/${maxRetries}) after ${delay}ms due to: ${err}`,