deepsource-mcp-server
Version:
Model Context Protocol server for DeepSource
329 lines • 10.8 kB
JavaScript
/**
* @fileoverview Circuit breaker implementation for fault tolerance
* This module implements the circuit breaker pattern to prevent cascade failures.
*/
import { createLogger } from '../logging/logger.js';
const logger = createLogger('CircuitBreaker');
/**
* Circuit breaker states
* @enum
* @public
*/
export var CircuitState;
(function (CircuitState) {
/** Normal operation - requests pass through */
CircuitState["CLOSED"] = "closed";
/** Circuit is open - requests fail immediately */
CircuitState["OPEN"] = "open";
/** Testing if service has recovered */
CircuitState["HALF_OPEN"] = "half-open";
})(CircuitState || (CircuitState = {}));
/**
* Default circuit breaker configuration from environment
* @private
*/
const DEFAULT_CONFIG = {
failureThreshold: parseInt(process.env.CIRCUIT_BREAKER_THRESHOLD || '5', 10),
failureWindow: 60000, // 1 minute
recoveryTimeout: parseInt(process.env.CIRCUIT_BREAKER_TIMEOUT_MS || '30000', 10),
successThreshold: 3,
halfOpenMaxAttempts: 5,
};
/**
* Circuit breaker implementation
* @class
* @public
*/
export class CircuitBreaker {
state = CircuitState.CLOSED;
failures = [];
successes = [];
lastStateChange = Date.now();
halfOpenAttempts = 0;
totalRequests = 0;
config;
name;
/**
* Creates a new circuit breaker instance
* @param name The name of this circuit breaker (for logging)
* @param config Optional configuration overrides
*/
constructor(name, config = {}) {
this.name = name;
this.config = { ...DEFAULT_CONFIG, ...config };
logger.debug('Circuit breaker created', { name, config: this.config });
}
/**
* Check if a request should be allowed through
* @returns True if the request can proceed
* @public
*/
canRequest() {
this.cleanupOldEntries();
switch (this.state) {
case CircuitState.CLOSED:
return true;
case CircuitState.OPEN: {
// Check if it's time to transition to half-open
const timeSinceOpen = Date.now() - this.lastStateChange;
if (timeSinceOpen >= this.config.recoveryTimeout) {
this.transitionTo(CircuitState.HALF_OPEN);
this.halfOpenAttempts = 1; // Count this request as the first attempt
return true;
}
logger.debug('Circuit breaker is open, rejecting request', {
name: this.name,
timeSinceOpen,
recoveryTimeout: this.config.recoveryTimeout,
});
return false;
}
case CircuitState.HALF_OPEN:
// Allow limited test requests
if (this.halfOpenAttempts < this.config.halfOpenMaxAttempts) {
this.halfOpenAttempts++;
return true;
}
logger.debug('Circuit breaker half-open limit reached', {
name: this.name,
attempts: this.halfOpenAttempts,
max: this.config.halfOpenMaxAttempts,
});
return false;
default:
return false;
}
}
/**
* Record a successful request
* @public
*/
recordSuccess() {
this.totalRequests++;
this.successes.push(Date.now());
this.cleanupOldEntries();
switch (this.state) {
case CircuitState.HALF_OPEN: {
// Check if we have enough successes to close the circuit
const recentSuccesses = this.getRecentSuccessCount();
logger.debug('Success in half-open state', {
name: this.name,
recentSuccesses,
threshold: this.config.successThreshold,
});
if (recentSuccesses >= this.config.successThreshold) {
this.transitionTo(CircuitState.CLOSED);
}
break;
}
case CircuitState.CLOSED:
// Normal operation, nothing special to do
break;
case CircuitState.OPEN:
// Shouldn't happen, but handle gracefully
logger.warn('Success recorded while circuit is open', { name: this.name });
break;
default:
// Log unexpected state
logger.error('Unexpected circuit state in recordSuccess', {
name: this.name,
state: this.state,
});
break;
}
}
/**
* Record a failed request
* @public
*/
recordFailure() {
this.totalRequests++;
this.failures.push(Date.now());
this.cleanupOldEntries();
switch (this.state) {
case CircuitState.CLOSED: {
// Check if we've exceeded the failure threshold
const recentFailures = this.getRecentFailureCount();
logger.debug('Failure in closed state', {
name: this.name,
recentFailures,
threshold: this.config.failureThreshold,
});
if (recentFailures >= this.config.failureThreshold) {
this.transitionTo(CircuitState.OPEN);
}
break;
}
case CircuitState.HALF_OPEN:
// Any failure in half-open state reopens the circuit
logger.debug('Failure in half-open state, reopening circuit', { name: this.name });
this.transitionTo(CircuitState.OPEN);
break;
case CircuitState.OPEN:
// Already open, nothing to do
break;
default:
// Log unexpected state
logger.error('Unexpected circuit state in recordFailure', {
name: this.name,
state: this.state,
});
break;
}
}
/**
* Get current circuit breaker statistics
* @returns The current statistics
* @public
*/
getStats() {
this.cleanupOldEntries();
const failureCount = this.getRecentFailureCount();
const successCount = this.getRecentSuccessCount();
const successRate = successCount + failureCount > 0 ? (successCount / (successCount + failureCount)) * 100 : 0;
const stats = {
state: this.state,
failureCount,
successCount,
totalRequests: this.totalRequests,
successRate: Math.round(successRate * 100) / 100,
};
if (this.state === CircuitState.OPEN) {
stats.lastOpenTime = this.lastStateChange;
}
if (this.state === CircuitState.CLOSED) {
stats.lastCloseTime = this.lastStateChange;
}
return stats;
}
/**
* Reset the circuit breaker to closed state
* @public
*/
reset() {
this.state = CircuitState.CLOSED;
this.failures = [];
this.successes = [];
this.halfOpenAttempts = 0;
this.lastStateChange = Date.now();
logger.info('Circuit breaker reset', { name: this.name });
}
/**
* Get the current state
* @returns The current circuit state
* @public
*/
getState() {
return this.state;
}
/**
* Transition to a new state
* @param newState The new state to transition to
* @private
*/
transitionTo(newState) {
const oldState = this.state;
this.state = newState;
this.lastStateChange = Date.now();
if (newState === CircuitState.HALF_OPEN) {
this.halfOpenAttempts = 0;
}
logger.info('Circuit breaker state transition', {
name: this.name,
from: oldState,
to: newState,
});
}
/**
* Clean up old entries outside the time window
* @private
*/
cleanupOldEntries() {
const cutoff = Date.now() - this.config.failureWindow;
this.failures = this.failures.filter((time) => time > cutoff);
this.successes = this.successes.filter((time) => time > cutoff);
}
/**
* Get the count of recent failures within the time window
* @returns The number of recent failures
* @private
*/
getRecentFailureCount() {
const cutoff = Date.now() - this.config.failureWindow;
return this.failures.filter((time) => time > cutoff).length;
}
/**
* Get the count of recent successes within the time window
* @returns The number of recent successes
* @private
*/
getRecentSuccessCount() {
const cutoff = Date.now() - this.config.failureWindow;
return this.successes.filter((time) => time > cutoff).length;
}
}
/**
* Circuit breaker manager for managing multiple circuit breakers
* @class
* @public
*/
export class CircuitBreakerManager {
static instance;
breakers = new Map();
/**
* Get the singleton instance
* @returns The circuit breaker manager instance
* @public
*/
static getInstance() {
if (!CircuitBreakerManager.instance) {
CircuitBreakerManager.instance = new CircuitBreakerManager();
}
return CircuitBreakerManager.instance;
}
/**
* Get or create a circuit breaker for an endpoint
* @param endpoint The endpoint name
* @param config Optional configuration overrides
* @returns The circuit breaker instance
* @public
*/
getBreaker(endpoint, config) {
let breaker = this.breakers.get(endpoint);
if (!breaker) {
breaker = new CircuitBreaker(endpoint, config);
this.breakers.set(endpoint, breaker);
}
return breaker;
}
/**
* Get statistics for all circuit breakers
* @returns Map of endpoint to statistics
* @public
*/
getAllStats() {
const stats = new Map();
for (const [endpoint, breaker] of this.breakers) {
stats.set(endpoint, breaker.getStats());
}
return stats;
}
/**
* Reset all circuit breakers
* @public
*/
resetAll() {
for (const breaker of this.breakers.values()) {
breaker.reset();
}
logger.info('All circuit breakers reset');
}
/**
* Clear all circuit breakers (for testing)
* @public
*/
clear() {
this.breakers.clear();
}
}
//# sourceMappingURL=circuit-breaker.js.map