mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 14:11:13 -05:00
Refactor: Remove circuit breaker system
This commit is contained in:
@@ -1,83 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { XCircle, Loader2 } from 'lucide-react';
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
|
||||||
import { useCircuitBreakerStatus } from '@/hooks/useCircuitBreakerStatus';
|
|
||||||
import { CircuitState } from '@/lib/circuitBreaker';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
export function NetworkStatusBanner() {
|
|
||||||
const { state, failureCount, isOpen, isHalfOpen } = useCircuitBreakerStatus();
|
|
||||||
const [countdown, setCountdown] = useState(60);
|
|
||||||
|
|
||||||
// Countdown for next retry attempt (when OPEN)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) return;
|
|
||||||
|
|
||||||
setCountdown(60); // Reset timeout from circuit breaker config
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setCountdown(prev => Math.max(0, prev - 1));
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
// Don't show if circuit is closed
|
|
||||||
if (state === CircuitState.CLOSED) return null;
|
|
||||||
|
|
||||||
// OPEN state - critical error
|
|
||||||
if (isOpen) {
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
role="alert"
|
|
||||||
aria-live="assertive"
|
|
||||||
aria-atomic="true"
|
|
||||||
variant="destructive"
|
|
||||||
className={cn(
|
|
||||||
"rounded-none border-x-0 border-t-0",
|
|
||||||
"animate-in slide-in-from-top-4 duration-300",
|
|
||||||
"bg-destructive/90 backdrop-blur-sm"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<XCircle className="h-5 w-5 animate-pulse" />
|
|
||||||
<AlertTitle className="font-semibold text-lg">
|
|
||||||
Database Unavailable
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription className="text-sm mt-1">
|
|
||||||
Our systems detected a service outage. <strong>Data and authentication are temporarily unavailable.</strong>
|
|
||||||
{' '}Retrying automatically... (next attempt in {countdown}s)
|
|
||||||
{failureCount > 0 && (
|
|
||||||
<span className="block mt-1 text-xs opacity-80">
|
|
||||||
{failureCount} failed connection attempts detected
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// HALF_OPEN state - testing recovery
|
|
||||||
if (isHalfOpen) {
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
role="alert"
|
|
||||||
aria-live="polite"
|
|
||||||
aria-atomic="true"
|
|
||||||
className={cn(
|
|
||||||
"rounded-none border-x-0 border-t-0",
|
|
||||||
"animate-in slide-in-from-top-4 duration-300",
|
|
||||||
"bg-amber-500/20 dark:bg-amber-500/30 border-amber-500"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Loader2 className="h-5 w-5 animate-spin text-amber-600 dark:text-amber-400" />
|
|
||||||
<AlertTitle className="font-semibold text-amber-900 dark:text-amber-100">
|
|
||||||
Connection Unstable
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription className="text-sm text-amber-800 dark:text-amber-200 mt-1">
|
|
||||||
Testing database connection... Some features may be slow or temporarily unavailable.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { supabaseCircuitBreaker, CircuitState } from '@/lib/circuitBreaker';
|
|
||||||
import { logger } from '@/lib/logger';
|
|
||||||
|
|
||||||
export function useCircuitBreakerStatus() {
|
|
||||||
const [state, setState] = useState<CircuitState>(CircuitState.CLOSED);
|
|
||||||
const [failureCount, setFailureCount] = useState(0);
|
|
||||||
const [lastStateChange, setLastStateChange] = useState<Date>(new Date());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Check immediately on mount
|
|
||||||
const checkState = () => {
|
|
||||||
const currentState = supabaseCircuitBreaker.getState();
|
|
||||||
const currentFailures = supabaseCircuitBreaker.getFailureCount();
|
|
||||||
|
|
||||||
setState(prevState => {
|
|
||||||
if (prevState !== currentState) {
|
|
||||||
setLastStateChange(new Date());
|
|
||||||
|
|
||||||
// Log state changes for monitoring
|
|
||||||
logger.info('Circuit breaker state changed', {
|
|
||||||
from: prevState,
|
|
||||||
to: currentState,
|
|
||||||
failureCount: currentFailures
|
|
||||||
});
|
|
||||||
|
|
||||||
// Emit custom event for other components
|
|
||||||
window.dispatchEvent(new CustomEvent('circuit-breaker-state-change', {
|
|
||||||
detail: { state: currentState, failureCount: currentFailures }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return currentState;
|
|
||||||
});
|
|
||||||
|
|
||||||
setFailureCount(currentFailures);
|
|
||||||
};
|
|
||||||
|
|
||||||
checkState();
|
|
||||||
|
|
||||||
// Poll every 5 seconds
|
|
||||||
const interval = setInterval(checkState, 5000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
failureCount,
|
|
||||||
lastStateChange,
|
|
||||||
isOpen: state === CircuitState.OPEN,
|
|
||||||
isHalfOpen: state === CircuitState.HALF_OPEN,
|
|
||||||
isClosed: state === CircuitState.CLOSED
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
/**
|
|
||||||
* Circuit Breaker Pattern Implementation
|
|
||||||
*
|
|
||||||
* Prevents cascade failures by temporarily blocking requests when service is degraded.
|
|
||||||
* Implements three states: CLOSED (normal), OPEN (blocking), HALF_OPEN (testing recovery)
|
|
||||||
*
|
|
||||||
* @see https://martinfowler.com/bliki/CircuitBreaker.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { logger } from './logger';
|
|
||||||
import { supabase } from './supabaseClient';
|
|
||||||
|
|
||||||
export interface CircuitBreakerConfig {
|
|
||||||
/** Number of failures before opening circuit (default: 5) */
|
|
||||||
failureThreshold: number;
|
|
||||||
/** Milliseconds to wait before testing recovery (default: 60000 = 1 min) */
|
|
||||||
resetTimeout: number;
|
|
||||||
/** Milliseconds window to track failures (default: 120000 = 2 min) */
|
|
||||||
monitoringWindow: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum CircuitState {
|
|
||||||
CLOSED = 'closed', // Normal operation, requests pass through
|
|
||||||
OPEN = 'open', // Failures detected, block all requests
|
|
||||||
HALF_OPEN = 'half_open' // Testing if service recovered
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CircuitBreaker {
|
|
||||||
private state: CircuitState = CircuitState.CLOSED;
|
|
||||||
private failures: number[] = []; // Timestamps of recent failures
|
|
||||||
private lastFailureTime: number | null = null;
|
|
||||||
private successCount: number = 0;
|
|
||||||
private config: Required<CircuitBreakerConfig>;
|
|
||||||
|
|
||||||
constructor(config: Partial<CircuitBreakerConfig> = {}) {
|
|
||||||
this.config = {
|
|
||||||
failureThreshold: config.failureThreshold ?? 5,
|
|
||||||
resetTimeout: config.resetTimeout ?? 60000,
|
|
||||||
monitoringWindow: config.monitoringWindow ?? 120000,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update configuration from admin settings
|
|
||||||
*/
|
|
||||||
async updateConfig(newConfig: Partial<CircuitBreakerConfig>): Promise<void> {
|
|
||||||
this.config = {
|
|
||||||
...this.config,
|
|
||||||
...newConfig
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.info('Circuit breaker config updated', { config: this.config });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a function through the circuit breaker
|
|
||||||
* @throws Error if circuit is OPEN (service unavailable)
|
|
||||||
*/
|
|
||||||
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
||||||
// If circuit is OPEN, check if we should transition to HALF_OPEN
|
|
||||||
if (this.state === CircuitState.OPEN) {
|
|
||||||
const timeSinceFailure = Date.now() - (this.lastFailureTime || 0);
|
|
||||||
|
|
||||||
if (timeSinceFailure > this.config.resetTimeout) {
|
|
||||||
this.state = CircuitState.HALF_OPEN;
|
|
||||||
this.successCount = 0;
|
|
||||||
logger.info('Circuit breaker transitioning to HALF_OPEN', {
|
|
||||||
timeSinceFailure,
|
|
||||||
resetTimeout: this.config.resetTimeout
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const timeRemaining = Math.ceil((this.config.resetTimeout - timeSinceFailure) / 1000);
|
|
||||||
throw new Error(
|
|
||||||
`Service temporarily unavailable. Our systems detected an outage and are protecting server resources. Please try again in ${timeRemaining} seconds.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await fn();
|
|
||||||
this.onSuccess();
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
this.onFailure();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onSuccess(): void {
|
|
||||||
if (this.state === CircuitState.HALF_OPEN) {
|
|
||||||
this.successCount++;
|
|
||||||
|
|
||||||
// After 2 successful requests, close the circuit
|
|
||||||
if (this.successCount >= 2) {
|
|
||||||
this.state = CircuitState.CLOSED;
|
|
||||||
this.failures = [];
|
|
||||||
this.lastFailureTime = null;
|
|
||||||
logger.info('Circuit breaker CLOSED - service recovered', {
|
|
||||||
successCount: this.successCount
|
|
||||||
});
|
|
||||||
|
|
||||||
// Emit event for UI components
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.dispatchEvent(new CustomEvent('circuit-breaker-closed', {
|
|
||||||
detail: { successCount: this.successCount }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onFailure(): void {
|
|
||||||
const now = Date.now();
|
|
||||||
this.lastFailureTime = now;
|
|
||||||
|
|
||||||
// Remove failures outside monitoring window
|
|
||||||
this.failures = this.failures.filter(
|
|
||||||
(timestamp) => now - timestamp < this.config.monitoringWindow
|
|
||||||
);
|
|
||||||
|
|
||||||
this.failures.push(now);
|
|
||||||
|
|
||||||
// Open circuit if threshold exceeded
|
|
||||||
if (this.failures.length >= this.config.failureThreshold) {
|
|
||||||
this.state = CircuitState.OPEN;
|
|
||||||
logger.error('Circuit breaker OPENED', {
|
|
||||||
failures: this.failures.length,
|
|
||||||
threshold: this.config.failureThreshold,
|
|
||||||
monitoringWindow: this.config.monitoringWindow,
|
|
||||||
resetTimeout: this.config.resetTimeout
|
|
||||||
});
|
|
||||||
|
|
||||||
// Emit event for UI components
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.dispatchEvent(new CustomEvent('circuit-breaker-opened', {
|
|
||||||
detail: {
|
|
||||||
failures: this.failures.length,
|
|
||||||
threshold: this.config.failureThreshold
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current circuit state
|
|
||||||
*/
|
|
||||||
getState(): CircuitState {
|
|
||||||
return this.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get number of recent failures
|
|
||||||
*/
|
|
||||||
getFailureCount(): number {
|
|
||||||
const now = Date.now();
|
|
||||||
return this.failures.filter(
|
|
||||||
(timestamp) => now - timestamp < this.config.monitoringWindow
|
|
||||||
).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force reset the circuit (testing/debugging only)
|
|
||||||
*/
|
|
||||||
reset(): void {
|
|
||||||
this.state = CircuitState.CLOSED;
|
|
||||||
this.failures = [];
|
|
||||||
this.lastFailureTime = null;
|
|
||||||
this.successCount = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load circuit breaker configuration from admin settings
|
|
||||||
* Falls back to defaults if settings unavailable
|
|
||||||
*/
|
|
||||||
export async function loadCircuitBreakerConfig(): Promise<CircuitBreakerConfig> {
|
|
||||||
try {
|
|
||||||
const { data: settings } = await supabase
|
|
||||||
.from('admin_settings')
|
|
||||||
.select('setting_key, setting_value')
|
|
||||||
.in('setting_key', [
|
|
||||||
'circuit_breaker.failure_threshold',
|
|
||||||
'circuit_breaker.reset_timeout',
|
|
||||||
'circuit_breaker.monitoring_window'
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!settings || settings.length === 0) {
|
|
||||||
return {
|
|
||||||
failureThreshold: 5,
|
|
||||||
resetTimeout: 60000,
|
|
||||||
monitoringWindow: 120000
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const config: any = {};
|
|
||||||
settings.forEach(s => {
|
|
||||||
const key = s.setting_key.replace('circuit_breaker.', '');
|
|
||||||
const camelKey = key.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
|
|
||||||
config[camelKey] = parseInt(String(s.setting_value));
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
failureThreshold: config.failureThreshold ?? 5,
|
|
||||||
resetTimeout: config.resetTimeout ?? 60000,
|
|
||||||
monitoringWindow: config.monitoringWindow ?? 120000
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to load circuit breaker config from admin settings', { error });
|
|
||||||
return {
|
|
||||||
failureThreshold: 5,
|
|
||||||
resetTimeout: 60000,
|
|
||||||
monitoringWindow: 120000
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Singleton circuit breaker for Supabase operations
|
|
||||||
* Shared across all submission flows to detect service-wide outages
|
|
||||||
*/
|
|
||||||
export const supabaseCircuitBreaker = new CircuitBreaker({
|
|
||||||
failureThreshold: 5,
|
|
||||||
resetTimeout: 60000,
|
|
||||||
monitoringWindow: 120000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load config from admin settings on startup
|
|
||||||
loadCircuitBreakerConfig().then(config => {
|
|
||||||
supabaseCircuitBreaker.updateConfig(config);
|
|
||||||
});
|
|
||||||
@@ -144,7 +144,6 @@ export const handleError = (
|
|||||||
isRetry: context.metadata?.isRetry || false,
|
isRetry: context.metadata?.isRetry || false,
|
||||||
attempt: context.metadata?.attempt,
|
attempt: context.metadata?.attempt,
|
||||||
retriesExhausted: context.metadata?.retriesExhausted || false,
|
retriesExhausted: context.metadata?.retriesExhausted || false,
|
||||||
circuitBreakerState: context.metadata?.circuitState,
|
|
||||||
supabaseError: supabaseErrorDetails,
|
supabaseError: supabaseErrorDetails,
|
||||||
metadata: context.metadata
|
metadata: context.metadata
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
import { supabaseCircuitBreaker } from './circuitBreaker';
|
|
||||||
import { supabase } from './supabaseClient';
|
import { supabase } from './supabaseClient';
|
||||||
|
|
||||||
export interface RetryOptions {
|
export interface RetryOptions {
|
||||||
@@ -216,10 +215,8 @@ export async function withRetry<T>(
|
|||||||
|
|
||||||
for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
|
for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
|
||||||
try {
|
try {
|
||||||
// Execute the function through circuit breaker
|
// Execute the function directly
|
||||||
const result = await supabaseCircuitBreaker.execute(async () => {
|
const result = await fn();
|
||||||
return await fn();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log successful retry if not first attempt
|
// Log successful retry if not first attempt
|
||||||
if (attempt > 0) {
|
if (attempt > 0) {
|
||||||
@@ -233,15 +230,6 @@ export async function withRetry<T>(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
|
|
||||||
// Check if circuit breaker blocked the request
|
|
||||||
if (error instanceof Error && error.message.includes('Circuit breaker is OPEN')) {
|
|
||||||
logger.error('Circuit breaker prevented retry', {
|
|
||||||
attempt: attempt + 1,
|
|
||||||
circuitState: supabaseCircuitBreaker.getState()
|
|
||||||
});
|
|
||||||
throw error; // Don't retry if circuit is open
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we should retry
|
// Check if we should retry
|
||||||
const isLastAttempt = attempt === config.maxAttempts - 1;
|
const isLastAttempt = attempt === config.maxAttempts - 1;
|
||||||
const shouldRetry = config.shouldRetry(error);
|
const shouldRetry = config.shouldRetry(error);
|
||||||
|
|||||||
Reference in New Issue
Block a user