180 lines
4.7 KiB
JavaScript
180 lines
4.7 KiB
JavaScript
/**
|
|
* Retry utilities with exponential backoff
|
|
*
|
|
* Features:
|
|
* - Simple retry with fixed delay
|
|
* - Exponential backoff with jitter
|
|
* - Callback hooks for logging/monitoring
|
|
*/
|
|
|
|
const sleep = (ms) => new Promise(res => setTimeout(res, ms));
|
|
|
|
/**
|
|
* Simple retry with fixed delay
|
|
*
|
|
* @param {Function} fn - Async function to retry
|
|
* @param {Object} options
|
|
* @param {number} options.retries - Number of retries (default: 3)
|
|
* @param {number} options.delayMs - Fixed delay between retries (default: 100)
|
|
* @param {Function} options.onRetry - Callback on retry attempt
|
|
* @returns {Promise} Result of fn
|
|
*/
|
|
export async function retry(fn, { retries = 3, delayMs = 100, onRetry } = {}) {
|
|
let lastErr;
|
|
|
|
for (let i = 0; i <= retries; i++) {
|
|
try {
|
|
return await fn();
|
|
} catch (err) {
|
|
lastErr = err;
|
|
|
|
if (i === retries) {
|
|
break;
|
|
}
|
|
|
|
if (onRetry) {
|
|
onRetry({ attempt: i + 1, maxAttempts: retries + 1, err, delay: delayMs });
|
|
}
|
|
|
|
await sleep(delayMs);
|
|
}
|
|
}
|
|
|
|
throw lastErr;
|
|
}
|
|
|
|
/**
|
|
* Retry with exponential backoff and jitter
|
|
*
|
|
* @param {Function} fn - Async function to retry
|
|
* @param {Object} options
|
|
* @param {number} options.retries - Number of retries (default: 3)
|
|
* @param {number} options.baseDelayMs - Base delay for exponential backoff (default: 100)
|
|
* @param {number} options.maxDelayMs - Maximum delay cap (default: 2000)
|
|
* @param {boolean} options.jitter - Add random jitter (default: true)
|
|
* @param {Function} options.onRetry - Callback on retry attempt
|
|
* @returns {Promise} Result of fn
|
|
*/
|
|
export async function retryWithBackoff(fn, {
|
|
retries = 3,
|
|
baseDelayMs = 100,
|
|
maxDelayMs = 2000,
|
|
jitter = true,
|
|
onRetry
|
|
} = {}) {
|
|
let lastErr;
|
|
|
|
for (let i = 0; i <= retries; i++) {
|
|
try {
|
|
return await fn();
|
|
} catch (err) {
|
|
lastErr = err;
|
|
|
|
if (i === retries) {
|
|
break;
|
|
}
|
|
|
|
// Exponential backoff: baseDelay * 2^attempt
|
|
const exponential = Math.min(maxDelayMs, baseDelayMs * Math.pow(2, i));
|
|
|
|
// Add jitter: random value between 50% and 100% of exponential
|
|
const delay = jitter
|
|
? Math.floor(exponential * (0.5 + Math.random() * 0.5))
|
|
: exponential;
|
|
|
|
if (onRetry) {
|
|
onRetry({
|
|
attempt: i + 1,
|
|
maxAttempts: retries + 1,
|
|
delay,
|
|
err,
|
|
exponential
|
|
});
|
|
}
|
|
|
|
await sleep(delay);
|
|
}
|
|
}
|
|
|
|
throw lastErr;
|
|
}
|
|
|
|
/**
|
|
* Retry with circuit breaker pattern
|
|
* Fails fast after consecutive errors threshold
|
|
*
|
|
* @param {Function} fn - Async function to retry
|
|
* @param {Object} options
|
|
* @param {number} options.retries - Retries per attempt (default: 2)
|
|
* @param {number} options.failureThreshold - Failures before circuit opens (default: 5)
|
|
* @param {number} options.resetTimeoutMs - Time before circuit half-open (default: 30000)
|
|
* @param {Function} options.onRetry - Callback on retry
|
|
* @param {Function} options.onCircuitOpen - Callback when circuit opens
|
|
* @returns {Promise} Result of fn
|
|
*/
|
|
export class CircuitBreaker {
|
|
constructor({
|
|
retries = 2,
|
|
failureThreshold = 5,
|
|
resetTimeoutMs = 30_000
|
|
} = {}) {
|
|
this.retries = retries;
|
|
this.failureThreshold = failureThreshold;
|
|
this.resetTimeoutMs = resetTimeoutMs;
|
|
this.failureCount = 0;
|
|
this.state = 'closed'; // 'closed' | 'open' | 'half-open'
|
|
this.lastFailureTime = null;
|
|
}
|
|
|
|
async execute(fn, { onRetry, onCircuitOpen } = {}) {
|
|
// Check if circuit should reset
|
|
if (this.state === 'open') {
|
|
const timeSinceFailure = Date.now() - this.lastFailureTime;
|
|
if (timeSinceFailure >= this.resetTimeoutMs) {
|
|
this.state = 'half-open';
|
|
this.failureCount = 0;
|
|
} else {
|
|
throw new Error(`Circuit breaker is open (reset in ${this.resetTimeoutMs - timeSinceFailure}ms)`);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const result = await retryWithBackoff(fn, { retries: this.retries, onRetry });
|
|
|
|
// Success: reset failure count
|
|
if (this.state === 'half-open') {
|
|
this.state = 'closed';
|
|
}
|
|
this.failureCount = 0;
|
|
|
|
return result;
|
|
} catch (err) {
|
|
this.failureCount++;
|
|
this.lastFailureTime = Date.now();
|
|
|
|
if (this.failureCount >= this.failureThreshold) {
|
|
this.state = 'open';
|
|
if (onCircuitOpen) {
|
|
onCircuitOpen({ failureCount: this.failureCount });
|
|
}
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
this.state = 'closed';
|
|
this.failureCount = 0;
|
|
this.lastFailureTime = null;
|
|
}
|
|
|
|
getState() {
|
|
return {
|
|
state: this.state,
|
|
failureCount: this.failureCount,
|
|
failureThreshold: this.failureThreshold
|
|
};
|
|
}
|
|
}
|