@daisugi/kintsugi
Version:
Kintsugi is a set of utilities to help build a fault tolerant services.
104 lines (94 loc) • 2.49 kB
text/typescript
import type {
AnzenAnyResult,
AnzenResultFn,
} from '@daisugi/anzen';
import { Ayamari } from '@daisugi/ayamari';
import { randomBetween } from './random_between.js';
import { waitFor } from './wait_for.js';
interface WithRetryOpts {
firstDelayMs?: number;
maxDelayMs?: number;
timeFactor?: number;
maxRetries?: number;
calculateRetryDelayMs?(
firstDelayMs: number,
maxDelayMs: number,
timeFactor: number,
retryNumber: number,
): number;
shouldRetry?(
response: any,
retryNumber: number,
maxRetries: number,
): boolean;
}
const defaultFirstDelayMs = 200;
const defaultMaxDelayMs = 600;
const defaultTimeFactor = 2;
const defaultMaxRetries = 3;
export function calculateRetryDelayMs(
firstDelayMs: number,
maxDelayMs: number,
timeFactor: number,
retryNumber: number,
) {
const delayMs = Math.min(
maxDelayMs,
firstDelayMs * timeFactor ** retryNumber,
);
/** Full jitter https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ */
const delayWithJitterMs = randomBetween(0, delayMs);
return delayWithJitterMs;
}
export function shouldRetry(
response: any,
retryNumber: number,
maxRetries: number,
) {
if (response.isFailure) {
if (
response.getError().code ===
Ayamari.errCode.CircuitSuspended
) {
return false;
}
if (retryNumber < maxRetries) {
return true;
}
}
return false;
}
export function withRetry(
fn: AnzenResultFn<any, any>,
opts: WithRetryOpts = {},
) {
const firstDelayMs =
opts.firstDelayMs || defaultFirstDelayMs;
const maxDelayMs = opts.maxDelayMs || defaultMaxDelayMs;
const timeFactor = opts.timeFactor || defaultTimeFactor;
const maxRetries = opts.maxRetries || defaultMaxRetries;
const _calculateRetryDelayMs =
opts.calculateRetryDelayMs || calculateRetryDelayMs;
const _shouldRetry = opts.shouldRetry || shouldRetry;
async function fnWithRetry(
this: unknown,
fn: AnzenResultFn<any, any>,
args: any[],
retryNumber: number,
): Promise<AnzenAnyResult<any, any>> {
const response = await fn.call(this, args);
if (_shouldRetry(response, retryNumber, maxRetries)) {
await waitFor(
_calculateRetryDelayMs(
firstDelayMs,
maxDelayMs,
timeFactor,
retryNumber,
),
);
return fnWithRetry(fn, args, retryNumber + 1);
}
return response;
}
return (...args: any[]) => fnWithRetry(fn, args, 0);
}