Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion src/internal/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -169,6 +192,7 @@ export interface ClientOptions {
credentialsProvider?: CredentialProvider
s3AccelerateEndpoint?: string
transportAgent?: http.Agent
retryOptions?: RetryOptions
}

export type RequestOption = Partial<IRequest> & {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
}
Expand Down
20 changes: 11 additions & 9 deletions src/internal/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, boolean> = {
Expand All @@ -52,17 +52,19 @@ 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(
transport: Transport,
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<http.IncomingMessage> {
let attempt = 0
let isRetryable = false
Expand All @@ -76,15 +78,15 @@ export async function requestWithRetry(
}

return response // Success, return the raw response
} catch (err) {
} catch (err: unknown) {
if (isRetryable) {
attempt++
isRetryable = false

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}`,
Expand Down
Loading