failover-sdk
Version:
One-line API failover with zero downtime. Native Rust performance with TypeScript interface.
391 lines • 14.5 kB
JavaScript
"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