mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 15:51:12 -05:00
feat: Implement retry logic and tracking
This commit is contained in:
142
supabase/functions/_shared/retryHelper.ts
Normal file
142
supabase/functions/_shared/retryHelper.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user