Skip to content

Commit 8dca7d3

Browse files
authored
fix: unwanted long retry due to very long default config (#1426)
1 parent 2e608ea commit 8dca7d3

File tree

2 files changed

+58
-10
lines changed

2 files changed

+58
-10
lines changed

src/internal/client.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,29 @@ const requestOptionProperties = [
155155
'sessionIdContext',
156156
] as const
157157

158+
export interface RetryOptions {
159+
/**
160+
* If this set to true, it will take precedence over all other retry options.
161+
* @default false
162+
*/
163+
disableRetry?: boolean
164+
/**
165+
* The maximum amount of retries for a request.
166+
* @default 1
167+
*/
168+
maximumRetryCount?: number
169+
/**
170+
* The minimum duration (in milliseconds) for the exponential backoff algorithm.
171+
* @default 100
172+
*/
173+
baseDelayMs?: number
174+
/**
175+
* The maximum duration (in milliseconds) for the exponential backoff algorithm.
176+
* @default 60000
177+
*/
178+
maximumDelayMs?: number
179+
}
180+
158181
export interface ClientOptions {
159182
endPoint: string
160183
accessKey?: string
@@ -169,6 +192,7 @@ export interface ClientOptions {
169192
credentialsProvider?: CredentialProvider
170193
s3AccelerateEndpoint?: string
171194
transportAgent?: http.Agent
195+
retryOptions?: RetryOptions
172196
}
173197

174198
export type RequestOption = Partial<IRequest> & {
@@ -212,6 +236,7 @@ export class TypedClient {
212236
protected credentialsProvider?: CredentialProvider
213237
partSize: number = 64 * 1024 * 1024
214238
protected overRidePartSize?: boolean
239+
protected retryOptions: RetryOptions
215240

216241
protected maximumPartSize = 5 * 1024 * 1024 * 1024
217242
protected maxObjectSize = 5 * 1024 * 1024 * 1024 * 1024
@@ -352,6 +377,20 @@ export class TypedClient {
352377
this.s3AccelerateEndpoint = params.s3AccelerateEndpoint || undefined
353378
this.reqOptions = {}
354379
this.clientExtensions = new Extensions(this)
380+
381+
if (params.retryOptions) {
382+
if (!isObject(params.retryOptions)) {
383+
throw new errors.InvalidArgumentError(
384+
`Invalid retryOptions type: ${params.retryOptions}, expected to be type "object"`,
385+
)
386+
}
387+
388+
this.retryOptions = params.retryOptions
389+
} else {
390+
this.retryOptions = {
391+
disableRetry: false,
392+
}
393+
}
355394
}
356395
/**
357396
* Minio extensions that aren't necessary present for Amazon S3 compatible storage servers
@@ -724,7 +763,14 @@ export class TypedClient {
724763
reqOptions.headers.authorization = signV4(reqOptions, this.accessKey, this.secretKey, region, date, sha256sum)
725764
}
726765

727-
const response = await requestWithRetry(this.transport, reqOptions, body)
766+
const response = await requestWithRetry(
767+
this.transport,
768+
reqOptions,
769+
body,
770+
this.retryOptions.disableRetry === true ? 0 : this.retryOptions.maximumRetryCount,
771+
this.retryOptions.baseDelayMs,
772+
this.retryOptions.maximumDelayMs,
773+
)
728774
if (!response.statusCode) {
729775
throw new Error("BUG: response doesn't have a statusCode")
730776
}

src/internal/request.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ export async function request(
2828
})
2929
}
3030

31-
const MAX_RETRIES = 10
32-
const EXP_BACK_OFF_BASE_DELAY = 1000 // Base delay for exponential backoff
33-
const ADDITIONAL_DELAY_FACTOR = 1.0 // to avoid synchronized retries
31+
const MAX_RETRIES = 1
32+
const BASE_DELAY_MS = 100 // Base delay for exponential backoff
33+
const MAX_DELAY_MS = 60000 // Max delay for exponential backoff
3434

3535
// Retryable error codes for HTTP ( ref: minio-go)
3636
export const retryHttpCodes: Record<string, boolean> = {
@@ -52,17 +52,19 @@ const sleep = (ms: number) => {
5252
return new Promise((resolve) => setTimeout(resolve, ms))
5353
}
5454

55-
const getExpBackOffDelay = (retryCount: number) => {
56-
const backOffBy = EXP_BACK_OFF_BASE_DELAY * 2 ** retryCount
57-
const additionalDelay = Math.random() * backOffBy * ADDITIONAL_DELAY_FACTOR
58-
return backOffBy + additionalDelay
55+
const getExpBackOffDelay = (retryCount: number, baseDelayMs: number, maximumDelayMs: number) => {
56+
const backOffBy = baseDelayMs * (1 << retryCount)
57+
const additionalDelay = Math.random() * backOffBy
58+
return Math.min(backOffBy + additionalDelay, maximumDelayMs)
5959
}
6060

6161
export async function requestWithRetry(
6262
transport: Transport,
6363
opt: https.RequestOptions,
6464
body: Buffer | string | stream.Readable | null = null,
6565
maxRetries: number = MAX_RETRIES,
66+
baseDelayMs: number = BASE_DELAY_MS,
67+
maximumDelayMs: number = MAX_DELAY_MS,
6668
): Promise<http.IncomingMessage> {
6769
let attempt = 0
6870
let isRetryable = false
@@ -76,15 +78,15 @@ export async function requestWithRetry(
7678
}
7779

7880
return response // Success, return the raw response
79-
} catch (err) {
81+
} catch (err: unknown) {
8082
if (isRetryable) {
8183
attempt++
8284
isRetryable = false
8385

8486
if (attempt > maxRetries) {
8587
throw new Error(`Request failed after ${maxRetries} retries: ${err}`)
8688
}
87-
const delay = getExpBackOffDelay(attempt)
89+
const delay = getExpBackOffDelay(attempt, baseDelayMs, maximumDelayMs)
8890
// eslint-disable-next-line no-console
8991
console.warn(
9092
`${new Date().toLocaleString()} Retrying request (attempt ${attempt}/${maxRetries}) after ${delay}ms due to: ${err}`,

0 commit comments

Comments
 (0)