UNPKG

trojanhorse-js

Version:

A comprehensive JavaScript library for fetching, managing, and analyzing global threat intelligence from multiple open-source feeds and security news sources. Unlike its mythological namesake, this Trojan protects your digital fortress.

655 lines (557 loc) 18.6 kB
/** * Circuit Breaker Pattern Implementation * * Provides fault tolerance and resilience for external API calls * with configurable failure thresholds, recovery timeouts, and monitoring. */ import { EventEmitter } from 'events'; export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN'; export interface CircuitBreakerConfig { failureThreshold: number; // Number of failures before opening successThreshold: number; // Number of successes to close from half-open timeout: number; // Time in ms before trying half-open monitoringWindow: number; // Time window for failure tracking volumeThreshold: number; // Minimum requests before considering failure rate errorFilter?: (error: Error) => boolean; // Filter which errors count as failures onStateChange?: (state: CircuitState) => void; // Callback for state changes } interface CircuitBreakerStats { state: CircuitState; failureCount: number; successCount: number; totalRequests: number; lastFailureTime: number | null; lastSuccessTime: number | null; stateChangeTime: number; requestStats: { total: number; failures: number; successes: number; timeouts: number; circuitOpen: number; }; responseTimeStats: { average: number; min: number; max: number; p95: number; p99: number; }; } interface RequestRecord { timestamp: number; success: boolean; responseTime: number; error?: string; } export class CircuitBreaker extends EventEmitter { private config: CircuitBreakerConfig; private state: CircuitState = 'CLOSED'; private failureCount: number = 0; private successCount: number = 0; private lastFailureTime: number | null = null; private lastSuccessTime: number | null = null; private stateChangeTime: number = Date.now(); private requestHistory: RequestRecord[] = []; private responseTimes: number[] = []; constructor(config: Partial<CircuitBreakerConfig> = {}) { super(); this.config = { failureThreshold: 5, successThreshold: 3, timeout: 60000, // 1 minute monitoringWindow: 60000, // 1 minute volumeThreshold: 10, errorFilter: () => true, // All errors count by default ...config }; // Cleanup old records periodically setInterval(() => this.cleanupOldRecords(), this.config.monitoringWindow); } /** * Execute a function with circuit breaker protection */ public async execute<T>(fn: () => Promise<T>): Promise<T> { if (this.state === 'OPEN') { if (this.shouldAttemptReset()) { this.setState('HALF_OPEN'); } else { const error = new Error('Circuit breaker is OPEN'); this.recordRequest(false, 0, error.message); throw error; } } const startTime = Date.now(); try { const result = await fn(); const responseTime = Date.now() - startTime; this.onSuccess(responseTime); return result; } catch (error) { const responseTime = Date.now() - startTime; this.onFailure(error as Error, responseTime); throw error; } } /** * Execute with automatic retry logic */ public async executeWithRetry<T>( fn: () => Promise<T>, maxRetries: number = 3, retryDelay: number = 1000 ): Promise<T> { let lastError: Error; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await this.execute(fn); } catch (error) { lastError = error as Error; // Don't retry if circuit is open if (this.state === 'OPEN') { throw error; } // Don't retry on last attempt if (attempt === maxRetries) { throw error; } // Exponential backoff const delay = retryDelay * Math.pow(2, attempt); await this.sleep(delay); } } throw lastError!; } /** * Execute multiple functions with circuit breaker protection */ public async executeBatch<T>( functions: Array<() => Promise<T>>, options: { maxConcurrency?: number; failFast?: boolean; continueOnFailure?: boolean; } = {} ): Promise<Array<{ success: boolean; result?: T; error?: Error }>> { const { maxConcurrency = 5, failFast = false, continueOnFailure = true } = options; const results: Array<{ success: boolean; result?: T; error?: Error }> = []; const executing: Promise<unknown>[] = []; for (let i = 0; i < functions.length; i++) { const fn = functions[i]; const executePromise: Promise<unknown> = fn ? this.execute(fn) : Promise.reject(new Error('No function provided')) .then(result => { results[i] = { success: true, result }; }) .catch(error => { results[i] = { success: false, error }; if (failFast && !continueOnFailure) { throw error; } }); executing.push(executePromise); // Limit concurrency if (executing.length >= maxConcurrency) { await Promise.race(executing); // Remove completed promises const completed = executing.filter(p => p === Promise.resolve() // This is a simplification ); completed.forEach(p => { const index = executing.indexOf(p); if (index > -1) { executing.splice(index, 1); } }); } } // Wait for all remaining executions await Promise.allSettled(executing); return results; } private onSuccess(responseTime: number): void { this.recordRequest(true, responseTime); this.lastSuccessTime = Date.now(); if (this.state === 'HALF_OPEN') { this.successCount++; if (this.successCount >= this.config.successThreshold) { this.setState('CLOSED'); this.reset(); } } else if (this.state === 'CLOSED') { this.failureCount = Math.max(0, this.failureCount - 1); } this.emit('success', { responseTime, state: this.state }); } private onFailure(error: Error, responseTime: number): void { // Check if this error should be counted if (this.config.errorFilter && !this.config.errorFilter(error)) { this.recordRequest(false, responseTime, 'filtered-error'); return; } this.recordRequest(false, responseTime, error.message); this.lastFailureTime = Date.now(); this.failureCount++; if (this.state === 'HALF_OPEN' || this.shouldOpen()) { this.setState('OPEN'); } this.emit('failure', { error: error.message, responseTime, state: this.state, failureCount: this.failureCount }); } private shouldOpen(): boolean { if (this.state === 'OPEN') { return false; } const recentRequests = this.getRecentRequests(); // Need minimum volume to consider opening if (recentRequests.length < this.config.volumeThreshold) { return false; } // Check failure rate return this.failureCount >= this.config.failureThreshold; } private shouldAttemptReset(): boolean { return this.lastFailureTime !== null && (Date.now() - this.lastFailureTime) >= this.config.timeout; } private setState(newState: CircuitState): void { if (this.state !== newState) { const oldState = this.state; this.state = newState; this.stateChangeTime = Date.now(); if (this.config.onStateChange) { this.config.onStateChange(newState); } this.emit('stateChange', { from: oldState, to: newState, timestamp: this.stateChangeTime }); // Log state changes console.log(`Circuit breaker state changed: ${oldState} -> ${newState}`); } } private reset(): void { this.failureCount = 0; this.successCount = 0; this.lastFailureTime = null; } private recordRequest(success: boolean, responseTime: number, error?: string): void { const record: RequestRecord = { timestamp: Date.now(), success, responseTime, error: error || '' }; this.requestHistory.push(record); this.responseTimes.push(responseTime); // Keep response times for percentile calculations if (this.responseTimes.length > 1000) { this.responseTimes = this.responseTimes.slice(-1000); } // Emit metrics this.emit('request', record); } private getRecentRequests(): RequestRecord[] { const cutoff = Date.now() - this.config.monitoringWindow; return this.requestHistory.filter(record => record.timestamp >= cutoff); } private cleanupOldRecords(): void { const cutoff = Date.now() - this.config.monitoringWindow; this.requestHistory = this.requestHistory.filter(record => record.timestamp >= cutoff ); } private calculatePercentile(values: number[], percentile: number): number { if (values.length === 0) { return 0; } const sorted = [...values].sort((a, b) => a - b); const index = Math.ceil((percentile / 100) * sorted.length) - 1; const validIndex = Math.max(0, Math.min(index, sorted.length - 1)); return sorted[validIndex] ?? 0; } private sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Get current circuit breaker statistics */ public getStats(): CircuitBreakerStats { const recentRequests = this.getRecentRequests(); const recentFailures = recentRequests.filter(r => !r.success); const recentSuccesses = recentRequests.filter(r => r.success); const responseTimeStats = this.responseTimes.length > 0 ? { average: this.responseTimes.reduce((a, b) => a + b, 0) / this.responseTimes.length, min: Math.min(...this.responseTimes), max: Math.max(...this.responseTimes), p95: this.calculatePercentile(this.responseTimes, 95), p99: this.calculatePercentile(this.responseTimes, 99) } : { average: 0, min: 0, max: 0, p95: 0, p99: 0 }; return { state: this.state, failureCount: this.failureCount, successCount: this.successCount, totalRequests: this.requestHistory.length, lastFailureTime: this.lastFailureTime, lastSuccessTime: this.lastSuccessTime, stateChangeTime: this.stateChangeTime, requestStats: { total: recentRequests.length, failures: recentFailures.length, successes: recentSuccesses.length, timeouts: recentFailures.filter(r => r.error?.includes('timeout')).length, circuitOpen: recentRequests.filter(r => r.error === 'Circuit breaker is OPEN').length }, responseTimeStats }; } /** * Get current state */ public getState(): CircuitState { return this.state; } /** * Get configuration */ public getConfig(): CircuitBreakerConfig { return { ...this.config }; } /** * Update configuration */ public updateConfig(newConfig: Partial<CircuitBreakerConfig>): void { this.config = { ...this.config, ...newConfig }; this.emit('configUpdate', this.config); } /** * Manually open the circuit */ public open(): void { this.setState('OPEN'); this.lastFailureTime = Date.now(); } /** * Manually close the circuit */ public close(): void { this.setState('CLOSED'); this.reset(); } /** * Force reset to half-open state */ public halfOpen(): void { this.setState('HALF_OPEN'); this.successCount = 0; } /** * Check if circuit is healthy */ public isHealthy(): boolean { if (this.state === 'OPEN') { return false; } const recentRequests = this.getRecentRequests(); if (recentRequests.length < this.config.volumeThreshold) { return true; } const failureRate = recentRequests.filter(r => !r.success).length / recentRequests.length; return failureRate < (this.config.failureThreshold / this.config.volumeThreshold); } /** * Get health score (0-100) */ public getHealthScore(): number { if (this.state === 'OPEN') { return 0; } const recentRequests = this.getRecentRequests(); if (recentRequests.length === 0) { return 100; } const successRate = recentRequests.filter(r => r.success).length / recentRequests.length; const baseScore = successRate * 100; // Adjust based on response times const avgResponseTime = this.responseTimes.reduce((a, b) => a + b, 0) / this.responseTimes.length; const responseTimePenalty = Math.min(avgResponseTime / 5000, 1) * 20; // Max 20 point penalty return Math.max(0, baseScore - responseTimePenalty); } /** * Export metrics for monitoring systems */ public exportMetrics(): Record<string, number> { const stats = this.getStats(); return { 'circuit_breaker_state': this.state === 'CLOSED' ? 0 : this.state === 'HALF_OPEN' ? 1 : 2, 'circuit_breaker_failure_count': stats.failureCount, 'circuit_breaker_success_count': stats.successCount, 'circuit_breaker_total_requests': stats.totalRequests, 'circuit_breaker_recent_failures': stats.requestStats.failures, 'circuit_breaker_recent_successes': stats.requestStats.successes, 'circuit_breaker_response_time_avg': stats.responseTimeStats.average, 'circuit_breaker_response_time_p95': stats.responseTimeStats.p95, 'circuit_breaker_response_time_p99': stats.responseTimeStats.p99, 'circuit_breaker_health_score': this.getHealthScore() }; } } /** * Circuit Breaker Manager for multiple services */ export class CircuitBreakerManager { private breakers: Map<string, CircuitBreaker> = new Map(); private globalConfig: Partial<CircuitBreakerConfig>; constructor(globalConfig: Partial<CircuitBreakerConfig> = {}) { this.globalConfig = globalConfig; } /** * Get or create a circuit breaker for a service */ public getBreaker(serviceName: string, config?: Partial<CircuitBreakerConfig>): CircuitBreaker { if (!this.breakers.has(serviceName)) { const breakerConfig = { ...this.globalConfig, ...config }; const breaker = new CircuitBreaker(breakerConfig); breaker.on('stateChange', (event) => { console.log(`[${serviceName}] Circuit breaker: ${event.from} -> ${event.to}`); }); this.breakers.set(serviceName, breaker); } return this.breakers.get(serviceName)!; } /** * Execute function with service-specific circuit breaker */ public async execute<T>( serviceName: string, fn: () => Promise<T>, config?: Partial<CircuitBreakerConfig> ): Promise<T> { const breaker = this.getBreaker(serviceName, config); return breaker.execute(fn); } /** * Get all circuit breaker stats */ public getAllStats(): Record<string, CircuitBreakerStats> { const stats: Record<string, CircuitBreakerStats> = {}; for (const [serviceName, breaker] of this.breakers) { stats[serviceName] = breaker.getStats(); } return stats; } /** * Get overall system health */ public getSystemHealth(): { healthy: number; degraded: number; unhealthy: number; totalServices: number; overallScore: number; } { let healthy = 0; let degraded = 0; let unhealthy = 0; let totalScore = 0; for (const breaker of this.breakers.values()) { const score = breaker.getHealthScore(); totalScore += score; if (score >= 80) { healthy++; } else if (score >= 50) { degraded++; } else { unhealthy++; } } return { healthy, degraded, unhealthy, totalServices: this.breakers.size, overallScore: this.breakers.size > 0 ? totalScore / this.breakers.size : 100 }; } /** * Reset all circuit breakers */ public resetAll(): void { for (const breaker of this.breakers.values()) { breaker.close(); } } /** * Remove a circuit breaker */ public removeBreaker(serviceName: string): boolean { return this.breakers.delete(serviceName); } } /** * Utility function to create a circuit breaker with common patterns */ export function createCircuitBreaker(pattern: 'aggressive' | 'conservative' | 'api' | 'database' | 'custom', customConfig?: Partial<CircuitBreakerConfig>): CircuitBreaker { let config: Partial<CircuitBreakerConfig>; switch (pattern) { case 'aggressive': config = { failureThreshold: 3, successThreshold: 2, timeout: 30000, // 30 seconds monitoringWindow: 30000, volumeThreshold: 5 }; break; case 'conservative': config = { failureThreshold: 10, successThreshold: 5, timeout: 120000, // 2 minutes monitoringWindow: 120000, volumeThreshold: 20 }; break; case 'api': config = { failureThreshold: 5, successThreshold: 3, timeout: 60000, // 1 minute monitoringWindow: 60000, volumeThreshold: 10, errorFilter: (error) => { // Don't count 4xx errors as circuit breaker failures return !error.message.includes('4') || error.message.includes('429'); } }; break; case 'database': config = { failureThreshold: 3, successThreshold: 2, timeout: 30000, monitoringWindow: 60000, volumeThreshold: 5, errorFilter: (error) => { // Count connection and timeout errors return error.message.includes('connection') || error.message.includes('timeout') || error.message.includes('ECONNREFUSED'); } }; break; default: // custom config = customConfig || {}; } return new CircuitBreaker({ ...config, ...customConfig }); }