@vfarcic/dot-ai
Version:
AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance
331 lines (330 loc) • 11.2 kB
JavaScript
"use strict";
/**
* Circuit Breaker Implementation
*
* Provides resilience for external service calls by preventing cascading failures.
* Implements the circuit breaker pattern with three states:
* - CLOSED: Normal operation, requests flow through
* - OPEN: Failure threshold reached, requests are blocked
* - HALF_OPEN: Testing recovery, limited requests allowed
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CircuitBreakerFactory = exports.CircuitBreaker = exports.CircuitOpenError = exports.CircuitState = void 0;
const error_handling_1 = require("./error-handling");
/**
* Circuit breaker states
*/
var CircuitState;
(function (CircuitState) {
CircuitState["CLOSED"] = "closed";
CircuitState["OPEN"] = "open";
CircuitState["HALF_OPEN"] = "half_open";
})(CircuitState || (exports.CircuitState = CircuitState = {}));
/**
* Error thrown when circuit is open and blocking requests
*/
class CircuitOpenError extends Error {
circuitName;
remainingCooldownMs;
state;
constructor(circuitName, remainingCooldownMs) {
super(`Circuit '${circuitName}' is open. Retry after ${Math.ceil(remainingCooldownMs)}ms`);
this.name = 'CircuitOpenError';
this.circuitName = circuitName;
this.remainingCooldownMs = remainingCooldownMs;
this.state = CircuitState.OPEN;
}
}
exports.CircuitOpenError = CircuitOpenError;
/**
* Circuit Breaker implementation
*
* Usage:
* ```typescript
* const breaker = new CircuitBreaker('embedding-api', { failureThreshold: 3 });
*
* try {
* const result = await breaker.execute(() => fetchFromAPI());
* } catch (error) {
* if (error instanceof CircuitOpenError) {
* // Circuit is open, handle gracefully
* }
* }
* ```
*/
class CircuitBreaker {
name;
config;
logger;
state = CircuitState.CLOSED;
consecutiveFailures = 0;
totalFailures = 0;
totalSuccesses = 0;
lastFailureTime;
lastSuccessTime;
openedAt;
halfOpenAttempts = 0;
lastCircuitOpenLogTime;
lastFailureLogTime;
suppressedFailureLogCount = 0;
/** Minimum interval between failure WARN logs (ms) */
static FAILURE_LOG_INTERVAL_MS = 30000;
constructor(name, config, logger) {
this.name = name;
this.config = {
failureThreshold: config?.failureThreshold ?? 3,
cooldownPeriodMs: config?.cooldownPeriodMs ?? 30000,
halfOpenMaxAttempts: config?.halfOpenMaxAttempts ?? 1,
onStateChange: config?.onStateChange,
};
this.logger = logger ?? new error_handling_1.ConsoleLogger('CircuitBreaker');
}
/**
* Execute an operation through the circuit breaker
* @throws CircuitOpenError if circuit is open
* @throws Original error if operation fails
*/
async execute(operation) {
// Check if we should allow the request
if (!this.canExecute()) {
const remainingCooldown = this.getRemainingCooldown();
// Only log once per circuit open period to avoid log spam
if (!this.lastCircuitOpenLogTime ||
this.lastCircuitOpenLogTime < this.openedAt) {
this.logger.warn(`Circuit '${this.name}' is open, blocking requests`, {
remainingCooldownMs: remainingCooldown,
willRetryAt: new Date(Date.now() + remainingCooldown).toISOString(),
});
this.lastCircuitOpenLogTime = new Date();
}
throw new CircuitOpenError(this.name, remainingCooldown);
}
// Track half-open attempts
if (this.state === CircuitState.HALF_OPEN) {
this.halfOpenAttempts++;
this.logger.info(`Circuit '${this.name}' attempting request in half-open state`, {
attempt: this.halfOpenAttempts,
maxAttempts: this.config.halfOpenMaxAttempts,
});
}
try {
const result = await operation();
this.recordSuccess();
return result;
}
catch (error) {
this.recordFailure(error);
throw error;
}
}
/**
* Record a successful operation
*/
recordSuccess() {
this.consecutiveFailures = 0;
this.totalSuccesses++;
this.lastSuccessTime = new Date();
if (this.state === CircuitState.HALF_OPEN) {
// Success in half-open state closes the circuit
this.transitionTo(CircuitState.CLOSED);
this.halfOpenAttempts = 0;
this.logger.info(`Circuit '${this.name}' recovered, transitioning to closed`, {
totalSuccesses: this.totalSuccesses,
});
}
}
/**
* Record a failed operation
*/
recordFailure(error) {
this.consecutiveFailures++;
this.totalFailures++;
this.lastFailureTime = new Date();
// Rate-limit failure WARN logs to avoid log spam during sustained outages
const now = Date.now();
const shouldLogFailure = !this.lastFailureLogTime ||
now - this.lastFailureLogTime.getTime() >=
CircuitBreaker.FAILURE_LOG_INTERVAL_MS;
if (shouldLogFailure) {
const logData = {
consecutiveFailures: this.consecutiveFailures,
threshold: this.config.failureThreshold,
error: error?.message,
};
if (this.suppressedFailureLogCount > 0) {
logData.suppressedLogCount = this.suppressedFailureLogCount;
}
this.logger.warn(`Circuit '${this.name}' recorded failure`, logData);
this.lastFailureLogTime = new Date(now);
this.suppressedFailureLogCount = 0;
}
else {
this.suppressedFailureLogCount++;
}
if (this.state === CircuitState.HALF_OPEN) {
// Failure in half-open state opens the circuit again
this.transitionTo(CircuitState.OPEN);
this.openedAt = new Date();
this.halfOpenAttempts = 0;
this.logger.warn(`Circuit '${this.name}' failed in half-open state, reopening`);
}
else if (this.state === CircuitState.CLOSED &&
this.consecutiveFailures >= this.config.failureThreshold) {
// Threshold reached, open the circuit
this.transitionTo(CircuitState.OPEN);
this.openedAt = new Date();
this.logger.error(`Circuit '${this.name}' opened after ${this.consecutiveFailures} consecutive failures`);
}
}
/**
* Reset the circuit breaker to initial state
*/
reset() {
const previousState = this.state;
this.state = CircuitState.CLOSED;
this.consecutiveFailures = 0;
this.halfOpenAttempts = 0;
this.openedAt = undefined;
this.lastCircuitOpenLogTime = undefined;
this.lastFailureLogTime = undefined;
this.suppressedFailureLogCount = 0;
if (previousState !== CircuitState.CLOSED) {
this.logger.info(`Circuit '${this.name}' manually reset to closed`);
this.config.onStateChange?.(previousState, CircuitState.CLOSED, this.name);
}
}
/**
* Get the current circuit state
*/
getState() {
// Check if we should transition from OPEN to HALF_OPEN
if (this.state === CircuitState.OPEN && this.shouldTransitionToHalfOpen()) {
this.transitionTo(CircuitState.HALF_OPEN);
this.halfOpenAttempts = 0;
this.logger.info(`Circuit '${this.name}' cooldown elapsed, transitioning to half-open`);
}
return this.state;
}
/**
* Get circuit breaker statistics
*/
getStats() {
return {
state: this.getState(),
consecutiveFailures: this.consecutiveFailures,
totalFailures: this.totalFailures,
totalSuccesses: this.totalSuccesses,
lastFailureTime: this.lastFailureTime,
lastSuccessTime: this.lastSuccessTime,
openedAt: this.openedAt,
halfOpenAttempts: this.halfOpenAttempts,
};
}
/**
* Check if circuit is currently open (blocking requests)
*/
isOpen() {
return this.getState() === CircuitState.OPEN;
}
/**
* Get the circuit breaker name
*/
getName() {
return this.name;
}
/**
* Check if a request can be executed
*/
canExecute() {
const currentState = this.getState();
switch (currentState) {
case CircuitState.CLOSED:
return true;
case CircuitState.OPEN:
return false;
case CircuitState.HALF_OPEN:
// Allow limited requests in half-open state
return this.halfOpenAttempts < this.config.halfOpenMaxAttempts;
default:
return false;
}
}
/**
* Check if cooldown period has elapsed
*/
shouldTransitionToHalfOpen() {
if (!this.openedAt) {
return false;
}
const elapsed = Date.now() - this.openedAt.getTime();
return elapsed >= this.config.cooldownPeriodMs;
}
/**
* Get remaining cooldown time in milliseconds
*/
getRemainingCooldown() {
if (!this.openedAt) {
return 0;
}
const elapsed = Date.now() - this.openedAt.getTime();
const remaining = this.config.cooldownPeriodMs - elapsed;
return Math.max(0, remaining);
}
/**
* Transition to a new state
*/
transitionTo(newState) {
const previousState = this.state;
this.state = newState;
this.config.onStateChange?.(previousState, newState, this.name);
}
}
exports.CircuitBreaker = CircuitBreaker;
/**
* Factory for creating circuit breakers with shared configuration
*/
class CircuitBreakerFactory {
defaultConfig;
logger;
breakers = new Map();
constructor(defaultConfig, logger) {
this.defaultConfig = defaultConfig ?? {};
this.logger = logger ?? new error_handling_1.ConsoleLogger('CircuitBreakerFactory');
}
/**
* Get or create a circuit breaker by name
*/
getOrCreate(name, config) {
let breaker = this.breakers.get(name);
if (!breaker) {
breaker = new CircuitBreaker(name, { ...this.defaultConfig, ...config }, this.logger);
this.breakers.set(name, breaker);
}
return breaker;
}
/**
* Get a circuit breaker by name (returns undefined if not exists)
*/
get(name) {
return this.breakers.get(name);
}
/**
* Reset all circuit breakers
*/
resetAll() {
for (const breaker of this.breakers.values()) {
breaker.reset();
}
}
/**
* Get stats for all circuit breakers
*/
getAllStats() {
const stats = {};
for (const [name, breaker] of this.breakers) {
stats[name] = breaker.getStats();
}
return stats;
}
}
exports.CircuitBreakerFactory = CircuitBreakerFactory;