From e08aacaff39d6c0cbbe3b2cc5eefdcdeefbaf0f3 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:02:17 +0000 Subject: [PATCH] Refactor: Remove circuit breaker system --- src/components/ui/network-status-banner.tsx | 83 ------- src/hooks/useCircuitBreakerStatus.ts | 54 ----- src/lib/circuitBreaker.ts | 231 -------------------- src/lib/errorHandler.ts | 1 - src/lib/retryHelpers.ts | 16 +- 5 files changed, 2 insertions(+), 383 deletions(-) delete mode 100644 src/components/ui/network-status-banner.tsx delete mode 100644 src/hooks/useCircuitBreakerStatus.ts delete mode 100644 src/lib/circuitBreaker.ts diff --git a/src/components/ui/network-status-banner.tsx b/src/components/ui/network-status-banner.tsx deleted file mode 100644 index 7eda7864..00000000 --- a/src/components/ui/network-status-banner.tsx +++ /dev/null @@ -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 ( - - - - Database Unavailable - - - Our systems detected a service outage. Data and authentication are temporarily unavailable. - {' '}Retrying automatically... (next attempt in {countdown}s) - {failureCount > 0 && ( - - {failureCount} failed connection attempts detected - - )} - - - ); - } - - // HALF_OPEN state - testing recovery - if (isHalfOpen) { - return ( - - - - Connection Unstable - - - Testing database connection... Some features may be slow or temporarily unavailable. - - - ); - } - - return null; -} diff --git a/src/hooks/useCircuitBreakerStatus.ts b/src/hooks/useCircuitBreakerStatus.ts deleted file mode 100644 index b645586e..00000000 --- a/src/hooks/useCircuitBreakerStatus.ts +++ /dev/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.CLOSED); - const [failureCount, setFailureCount] = useState(0); - const [lastStateChange, setLastStateChange] = useState(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 - }; -} diff --git a/src/lib/circuitBreaker.ts b/src/lib/circuitBreaker.ts deleted file mode 100644 index 72f508bf..00000000 --- a/src/lib/circuitBreaker.ts +++ /dev/null @@ -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; - - constructor(config: Partial = {}) { - this.config = { - failureThreshold: config.failureThreshold ?? 5, - resetTimeout: config.resetTimeout ?? 60000, - monitoringWindow: config.monitoringWindow ?? 120000, - }; - } - - /** - * Update configuration from admin settings - */ - async updateConfig(newConfig: Partial): Promise { - 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(fn: () => Promise): Promise { - // 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 { - 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); -}); diff --git a/src/lib/errorHandler.ts b/src/lib/errorHandler.ts index d24f1d51..255c8572 100644 --- a/src/lib/errorHandler.ts +++ b/src/lib/errorHandler.ts @@ -144,7 +144,6 @@ export const handleError = ( isRetry: context.metadata?.isRetry || false, attempt: context.metadata?.attempt, retriesExhausted: context.metadata?.retriesExhausted || false, - circuitBreakerState: context.metadata?.circuitState, supabaseError: supabaseErrorDetails, metadata: context.metadata }), diff --git a/src/lib/retryHelpers.ts b/src/lib/retryHelpers.ts index 186a0d0d..643ad6bd 100644 --- a/src/lib/retryHelpers.ts +++ b/src/lib/retryHelpers.ts @@ -4,7 +4,6 @@ */ import { logger } from './logger'; -import { supabaseCircuitBreaker } from './circuitBreaker'; import { supabase } from './supabaseClient'; export interface RetryOptions { @@ -216,10 +215,8 @@ export async function withRetry( for (let attempt = 0; attempt < config.maxAttempts; attempt++) { try { - // Execute the function through circuit breaker - const result = await supabaseCircuitBreaker.execute(async () => { - return await fn(); - }); + // Execute the function directly + const result = await fn(); // Log successful retry if not first attempt if (attempt > 0) { @@ -233,15 +230,6 @@ export async function withRetry( } catch (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 const isLastAttempt = attempt === config.maxAttempts - 1; const shouldRetry = config.shouldRetry(error);