UNPKG

failover-sdk

Version:

One-line API failover with zero downtime. Native Rust performance with TypeScript interface.

391 lines 14.5 kB
"use strict"; // Circuit Breaker - Native Rust implementation with JavaScript fallback // Uses built-in native module for maximum performance, falls back to JS when not available Object.defineProperty(exports, "__esModule", { value: true }); exports.CircuitBreakerInstance = exports.CircuitBreakerManager = exports.CircuitBreakerError = exports.CircuitState = void 0; // Try to load native Rust implementation (built into this package) let nativeCore = null; function loadNativeModule() { if (nativeCore !== null) return nativeCore; try { // Try to load the native module - NAPI-RS handles platform detection const { __napiModule } = require('failover-sdk'); if (__napiModule) { nativeCore = __napiModule.exports; console.log('[CircuitBreaker] Using native Rust implementation for maximum performance'); } } catch (error) { // Try alternative loading method try { nativeCore = require('../index.js'); console.log('[CircuitBreaker] Using native Rust implementation for maximum performance'); } catch (error2) { console.log('[CircuitBreaker] Native module not available, using JavaScript fallback'); nativeCore = null; } } return nativeCore; } var CircuitState; (function (CircuitState) { CircuitState["Closed"] = "Closed"; CircuitState["Open"] = "Open"; CircuitState["HalfOpen"] = "HalfOpen"; })(CircuitState || (exports.CircuitState = CircuitState = {})); class CircuitBreakerInstance { constructor(config) { this.recent_requests = []; this.MAX_RECENT_REQUESTS = 100; // Keep last 100 requests for analysis this.config = { failure_threshold: 5, success_threshold: 3, timeout_duration_ms: 30000, // 30 seconds request_timeout_ms: 10000, // 10 seconds min_requests: 10, failure_rate_threshold: 0.5, // 50% ...config, }; this.stats = { state: CircuitState.Closed, failure_count: 0, success_count: 0, total_requests: 0, consecutive_failures: 0, consecutive_successes: 0, state_changed_at: Date.now(), average_response_time_ms: 0, }; } async execute(operation) { if (!this.canExecute()) { throw new CircuitBreakerError(`Circuit breaker is open, rejecting request`); } const startTime = Date.now(); try { const result = await Promise.race([ operation(), new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), this.config.request_timeout_ms)), ]); this.onSuccess(Date.now() - startTime); return result; } catch (error) { this.onFailure(Date.now() - startTime, error.message); throw error; } } canExecute() { const now = Date.now(); switch (this.stats.state) { case CircuitState.Closed: return true; case CircuitState.Open: // Check if timeout has elapsed to move to half-open if (now - this.stats.state_changed_at >= this.config.timeout_duration_ms) { this.transitionTo(CircuitState.HalfOpen); return true; } return false; case CircuitState.HalfOpen: return true; default: return false; } } onSuccess(response_time_ms) { this.stats.success_count++; this.stats.total_requests++; this.stats.consecutive_successes++; this.stats.consecutive_failures = 0; this.stats.last_success_time = Date.now(); this.addRequestResult({ success: true, timestamp: Date.now(), response_time_ms, }); this.updateAverageResponseTime(); // State transitions based on successes if (this.stats.state === CircuitState.HalfOpen) { if (this.stats.consecutive_successes >= this.config.success_threshold) { this.transitionTo(CircuitState.Closed); } } } onFailure(response_time_ms, error_type) { this.stats.failure_count++; this.stats.total_requests++; this.stats.consecutive_failures++; this.stats.consecutive_successes = 0; this.stats.last_failure_time = Date.now(); this.addRequestResult({ success: false, timestamp: Date.now(), response_time_ms, error_type, }); this.updateAverageResponseTime(); // State transitions based on failures if (this.stats.state === CircuitState.Closed || this.stats.state === CircuitState.HalfOpen) { if (this.shouldOpenCircuit()) { this.transitionTo(CircuitState.Open); } } } shouldOpenCircuit() { // Check minimum requests threshold if (this.stats.total_requests < this.config.min_requests) { return false; } // Check consecutive failures threshold if (this.stats.consecutive_failures >= this.config.failure_threshold) { return true; } // Check failure rate threshold const failure_rate = this.stats.failure_count / this.stats.total_requests; return failure_rate >= this.config.failure_rate_threshold; } transitionTo(new_state) { if (this.stats.state !== new_state) { const old_state = this.stats.state; this.stats.state = new_state; this.stats.state_changed_at = Date.now(); // Reset counters on state transitions if (new_state === CircuitState.Closed) { this.stats.consecutive_failures = 0; } else if (new_state === CircuitState.HalfOpen) { this.stats.consecutive_successes = 0; } console.log(`[CircuitBreaker] State transition: ${old_state} -> ${new_state}`); } } addRequestResult(result) { this.recent_requests.push(result); // Keep only the most recent requests if (this.recent_requests.length > this.MAX_RECENT_REQUESTS) { this.recent_requests = this.recent_requests.slice(-this.MAX_RECENT_REQUESTS); } } updateAverageResponseTime() { if (this.recent_requests.length === 0) { this.stats.average_response_time_ms = 0; return; } const total_time = this.recent_requests.reduce((sum, req) => sum + req.response_time_ms, 0); this.stats.average_response_time_ms = total_time / this.recent_requests.length; } getStats() { return { ...this.stats }; } reset() { this.stats = { state: CircuitState.Closed, failure_count: 0, success_count: 0, total_requests: 0, consecutive_failures: 0, consecutive_successes: 0, state_changed_at: Date.now(), average_response_time_ms: 0, }; this.recent_requests = []; } trip() { this.transitionTo(CircuitState.Open); } getConfig() { return { ...this.config }; } } exports.CircuitBreakerInstance = CircuitBreakerInstance; class CircuitBreakerError extends Error { constructor(message) { super(message); this.name = 'CircuitBreakerError'; } } exports.CircuitBreakerError = CircuitBreakerError; // Global circuit breaker registry const circuitBreakers = new Map(); class CircuitBreakerManager { /** * Get or create a circuit breaker for a provider */ static getCircuitBreaker(provider, config) { if (!circuitBreakers.has(provider)) { circuitBreakers.set(provider, new CircuitBreakerInstance(config)); } return circuitBreakers.get(provider); } /** * Execute an operation with circuit breaker protection */ static async execute(provider, operation, config) { const circuitBreaker = this.getCircuitBreaker(provider, config); return await circuitBreaker.execute(operation); } /** * Get circuit breaker statistics for a specific provider */ static getStats(provider) { // Try to use native implementation if available const native = loadNativeModule(); if (native && native.getCircuitBreakerStats) { try { const statsJson = native.getCircuitBreakerStats(provider); if (statsJson) { return JSON.parse(statsJson); } } catch (error) { console.warn(`[CircuitBreaker] Native stats failed for ${provider}, falling back to JS:`, error); } } // Fallback to JavaScript implementation const circuitBreaker = circuitBreakers.get(provider); return circuitBreaker ? circuitBreaker.getStats() : null; } /** * Get circuit breaker statistics for all providers */ static getAllStats() { const allStats = {}; for (const [provider, circuitBreaker] of circuitBreakers) { allStats[provider] = circuitBreaker.getStats(); } return allStats; } /** * Get health score for a specific provider (0.0 = unhealthy, 1.0 = healthy) */ static getHealthScore(provider) { const stats = this.getStats(provider); if (!stats || stats.total_requests === 0) { return 1.0; // Assume healthy if no data } // Base score on success rate const success_rate = stats.success_count / stats.total_requests; // Apply penalties for circuit state let health_score = success_rate; if (stats.state === CircuitState.Open) { health_score *= 0.1; // Heavy penalty for open circuit } else if (stats.state === CircuitState.HalfOpen) { health_score *= 0.5; // Moderate penalty for half-open circuit } // Apply penalty for high response times if (stats.average_response_time_ms > 5000) { // > 5 seconds health_score *= 0.7; } else if (stats.average_response_time_ms > 2000) { // > 2 seconds health_score *= 0.9; } return Math.max(0, Math.min(1, health_score)); } /** * Get health scores for multiple providers */ static getHealthScores(providers) { const scores = {}; for (const provider of providers) { scores[provider] = this.getHealthScore(provider); } return scores; } /** * Reset circuit breaker for a specific provider (back to closed state) */ static reset(provider) { const circuitBreaker = circuitBreakers.get(provider); if (circuitBreaker) { circuitBreaker.reset(); } } /** * Manually trip the circuit breaker for a specific provider (force to open state) */ static trip(provider) { const circuitBreaker = this.getCircuitBreaker(provider); circuitBreaker.trip(); } /** * Check if a provider's circuit breaker is healthy (closed state) */ static isHealthy(provider) { const stats = this.getStats(provider); return !stats || stats.state === CircuitState.Closed; } /** * Get a summary of all circuit breakers */ static getSummary() { const summary = {}; for (const [provider, circuitBreaker] of circuitBreakers) { const stats = circuitBreaker.getStats(); summary[provider] = { state: stats.state, health_score: this.getHealthScore(provider), total_requests: stats.total_requests, }; } return summary; } /** * Monitor circuit breaker events (returns a function to unsubscribe) */ static monitor(callback) { // Simple monitoring implementation // In a production system, this would use event emitters const interval = setInterval(() => { const summary = this.getSummary(); for (const [provider, data] of Object.entries(summary)) { if (data.state === CircuitState.Open) { callback(provider, 'circuit_open', data); } } }, 5000); // Check every 5 seconds return () => clearInterval(interval); } /** * Get recommendations for circuit breaker configuration */ static getRecommendations(provider) { const circuitBreaker = circuitBreakers.get(provider); if (!circuitBreaker) { return { current_config: new CircuitBreakerInstance().getConfig(), recommendations: ['No data available - circuit breaker not initialized'], }; } const stats = circuitBreaker.getStats(); const config = circuitBreaker.getConfig(); const recommendations = []; // Analyze performance and provide recommendations if (stats.total_requests > 100) { const failure_rate = stats.failure_count / stats.total_requests; if (failure_rate > 0.3 && config.failure_rate_threshold > 0.3) { recommendations.push('Consider lowering failure_rate_threshold to trip circuit earlier'); } if (stats.average_response_time_ms > 5000 && config.request_timeout_ms > 10000) { recommendations.push('Consider lowering request_timeout_ms for faster failure detection'); } if (stats.state === CircuitState.Open && config.timeout_duration_ms < 60000) { recommendations.push('Consider increasing timeout_duration_ms if service takes time to recover'); } } else { recommendations.push('Insufficient data for meaningful recommendations'); } return { current_config: config, recommendations: recommendations.length > 0 ? recommendations : ['Configuration appears optimal'], }; } } exports.CircuitBreakerManager = CircuitBreakerManager; //# sourceMappingURL=circuit-breaker.js.map