-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(retry): copy retry middleware to SDK and fix it
- Loading branch information
1 parent
c8c3af8
commit 66b45a3
Showing
5 changed files
with
173 additions
and
52 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import { | ||
delay, | ||
rethrowAbortError, | ||
} from 'abort-controller-x'; | ||
import { | ||
ClientError, | ||
ClientMiddleware, | ||
Status, | ||
} from 'nice-grpc'; | ||
import { AbortController } from 'node-abort-controller'; | ||
|
||
/** | ||
* These options are added to `CallOptions` by | ||
* `nice-grpc-client-middleware-retry`. | ||
*/ | ||
export type RetryOptions = { | ||
/** | ||
* Boolean indicating whether retries are enabled. | ||
* | ||
* If the method is marked as idempotent in Protobuf, i.e. has | ||
* | ||
* option idempotency_level = IDEMPOTENT; | ||
* | ||
* then the default is `true`. Otherwise, the default is `false`. | ||
* | ||
* Method options currently work only when compiling with `ts-proto`. | ||
*/ | ||
retry?: boolean; | ||
/** | ||
* Base delay between retry attempts in milliseconds. | ||
* | ||
* Defaults to 1000. | ||
* | ||
* Example: if `retryBaseDelayMs` is 100, then retries will be attempted in | ||
* 100ms, 200ms, 400ms etc. (not counting jitter). | ||
*/ | ||
retryBaseDelayMs?: number; | ||
/** | ||
* Maximum delay between attempts in milliseconds. | ||
* | ||
* Defaults to 15 seconds. | ||
* | ||
* Example: if `retryBaseDelayMs` is 1000 and `retryMaxDelayMs` is 3000, then | ||
* retries will be attempted in 1000ms, 2000ms, 3000ms, 3000ms etc (not | ||
* counting jitter). | ||
*/ | ||
retryMaxDelayMs?: number; | ||
/** | ||
* Maximum for the total number of attempts. `Infinity` is supported. | ||
* | ||
* Defaults to 1, i.e. a single retry will be attempted. | ||
*/ | ||
retryMaxAttempts?: number; | ||
/** | ||
* Array of retryable status codes. | ||
* | ||
* Default is `[UNKNOWN, RESOURCE_EXHAUSTED, INTERNAL, UNAVAILABLE]`. | ||
*/ | ||
retryableStatuses?: Status[]; | ||
/** | ||
* Called after receiving error with retryable status code before setting | ||
* backoff delay timer. | ||
* | ||
* If the error code is not retryable, or the maximum attempts exceeded, this | ||
* function will not be called and the error will be thrown from the client | ||
* method. | ||
*/ | ||
onRetryableError?(error: ClientError, attempt: number, delayMs: number): void; | ||
}; | ||
|
||
const defaultRetryableStatuses: Status[] = [ | ||
Status.UNKNOWN, | ||
Status.RESOURCE_EXHAUSTED, | ||
Status.INTERNAL, | ||
Status.UNAVAILABLE, | ||
]; | ||
|
||
/** | ||
* Client middleware that adds automatic retries to unary calls. | ||
*/ | ||
export const retryMiddleware: ClientMiddleware<RetryOptions> = async function* retryMiddleware(call, options) { | ||
const { idempotencyLevel } = call.method.options ?? {}; | ||
const isIdempotent = idempotencyLevel === 'IDEMPOTENT' | ||
|| idempotencyLevel === 'NO_SIDE_EFFECTS'; | ||
|
||
const { | ||
retry = isIdempotent, | ||
retryBaseDelayMs = 1000, | ||
retryMaxDelayMs = 15_000, | ||
retryMaxAttempts = 1, | ||
onRetryableError, | ||
retryableStatuses = defaultRetryableStatuses, | ||
...restOptions | ||
} = options; | ||
|
||
if (call.requestStream || call.responseStream || !retry) { | ||
return yield* call.next(call.request, restOptions); | ||
} | ||
|
||
const signal = options.signal ?? new AbortController().signal; | ||
|
||
for (let attempt = 0; ; attempt++) { | ||
try { | ||
return yield* call.next(call.request, restOptions); | ||
} catch (error: unknown) { | ||
rethrowAbortError(error); | ||
|
||
if ( | ||
attempt >= retryMaxAttempts | ||
|| !(error instanceof ClientError) | ||
|| !retryableStatuses.includes(error.code) | ||
) { | ||
throw error; | ||
} | ||
|
||
// https://aws.amazon.com/ru/blogs/architecture/exponential-backoff-and-jitter/ | ||
const backoff = Math.min( | ||
retryMaxDelayMs, | ||
2 ** attempt * retryBaseDelayMs, | ||
); | ||
const delayMs = Math.round((backoff * (1 + Math.random())) / 2); | ||
|
||
onRetryableError?.(error, attempt, delayMs); | ||
|
||
// eslint-disable-next-line no-await-in-loop | ||
await delay(signal, delayMs); | ||
} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters