feat: Implement retry logic and tracking

This commit is contained in:
gpt-engineer-app[bot]
2025-11-05 20:19:43 +00:00
parent 028ea433bb
commit c8018b827e
8 changed files with 361 additions and 139 deletions

View File

@@ -0,0 +1,142 @@
/**
* Edge Function Retry Helper
* Provides exponential backoff retry logic for external API calls
*/
import { edgeLogger } from './logger.ts';
export interface EdgeRetryOptions {
maxAttempts?: number;
baseDelay?: number;
maxDelay?: number;
backoffMultiplier?: number;
jitter?: boolean;
shouldRetry?: (error: unknown) => boolean;
}
/**
* Determines if an error is transient and should be retried
*/
export function isRetryableError(error: unknown): boolean {
// Network errors
if (error instanceof TypeError && error.message.includes('fetch')) return true;
if (error instanceof Error) {
const msg = error.message.toLowerCase();
if (msg.includes('network') || msg.includes('timeout') || msg.includes('econnrefused')) {
return true;
}
}
// HTTP status codes that should be retried
if (error && typeof error === 'object') {
const httpError = error as { status?: number };
// Rate limiting
if (httpError.status === 429) return true;
// Service unavailable or gateway timeout
if (httpError.status === 503 || httpError.status === 504) return true;
// Server errors (5xx)
if (httpError.status && httpError.status >= 500 && httpError.status < 600) {
return true;
}
}
return false;
}
/**
* Calculate exponential backoff delay with optional jitter
*/
function calculateBackoffDelay(
attempt: number,
baseDelay: number,
maxDelay: number,
backoffMultiplier: number,
jitter: boolean
): number {
const exponentialDelay = baseDelay * Math.pow(backoffMultiplier, attempt);
const cappedDelay = Math.min(exponentialDelay, maxDelay);
if (!jitter) return cappedDelay;
// Add random jitter (-30% to +30%)
const jitterAmount = cappedDelay * 0.3;
const randomJitter = (Math.random() * 2 - 1) * jitterAmount;
return Math.max(0, cappedDelay + randomJitter);
}
/**
* Retry wrapper for asynchronous operations with exponential backoff
*
* @param fn - Async function to retry
* @param options - Retry configuration
* @param requestId - Request ID for tracking
* @param context - Context description for logging
*/
export async function withEdgeRetry<T>(
fn: () => Promise<T>,
options: EdgeRetryOptions = {},
requestId: string,
context: string
): Promise<T> {
const maxAttempts = options.maxAttempts ?? 3;
const baseDelay = options.baseDelay ?? 1000;
const maxDelay = options.maxDelay ?? 10000;
const backoffMultiplier = options.backoffMultiplier ?? 2;
const jitter = options.jitter ?? true;
const shouldRetry = options.shouldRetry ?? isRetryableError;
let lastError: unknown;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
// Don't retry if this is the last attempt
if (attempt === maxAttempts - 1) {
edgeLogger.error('All retry attempts exhausted', {
requestId,
context,
attempts: maxAttempts,
error: error instanceof Error ? error.message : 'Unknown error'
});
throw error;
}
// Don't retry if error is not retryable
if (!shouldRetry(error)) {
edgeLogger.info('Error not retryable, failing immediately', {
requestId,
context,
attempt: attempt + 1,
error: error instanceof Error ? error.message : 'Unknown error'
});
throw error;
}
// Calculate delay for next retry
const delay = calculateBackoffDelay(attempt, baseDelay, maxDelay, backoffMultiplier, jitter);
edgeLogger.info('Retrying after error', {
requestId,
context,
attempt: attempt + 1,
maxAttempts,
delay: Math.round(delay),
error: error instanceof Error ? error.message : 'Unknown error'
});
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delay));
}
}
// This should never be reached, but TypeScript needs it
throw lastError;
}