Files
elasticsearch/ts/core/connection/circuit-breaker.ts

307 lines
6.9 KiB
TypeScript

/**
* Circuit breaker states
*/
export enum CircuitState {
/** Circuit is closed, requests flow normally */
CLOSED = 'closed',
/** Circuit is open, requests are rejected immediately */
OPEN = 'open',
/** Circuit is half-open, testing if service recovered */
HALF_OPEN = 'half_open',
}
/**
* Circuit breaker configuration
*/
export interface CircuitBreakerConfig {
/** Number of failures before opening circuit */
failureThreshold: number;
/** Number of successes in half-open state before closing */
successThreshold: number;
/** Time in milliseconds circuit stays open before attempting half-open */
timeout: number;
/** Time window in milliseconds for counting failures */
rollingWindow: number;
/** Whether circuit breaker is enabled */
enabled: boolean;
}
/**
* Default circuit breaker configuration
*/
export const DEFAULT_CIRCUIT_BREAKER_CONFIG: CircuitBreakerConfig = {
failureThreshold: 5,
successThreshold: 2,
timeout: 60000, // 1 minute
rollingWindow: 10000, // 10 seconds
enabled: true,
};
/**
* Circuit breaker error thrown when circuit is open
*/
export class CircuitBreakerOpenError extends Error {
constructor(
public readonly circuitName: string,
public readonly nextAttemptTime: Date
) {
super(
`Circuit breaker "${circuitName}" is OPEN. Next attempt at ${nextAttemptTime.toISOString()}`
);
this.name = 'CircuitBreakerOpenError';
}
}
/**
* Failure record for tracking
*/
interface FailureRecord {
timestamp: number;
error: Error;
}
/**
* Circuit breaker for preventing cascading failures
*
* @example
* ```typescript
* const breaker = new CircuitBreaker('elasticsearch', {
* failureThreshold: 5,
* timeout: 60000,
* });
*
* try {
* const result = await breaker.execute(async () => {
* return await someElasticsearchOperation();
* });
* } catch (error) {
* if (error instanceof CircuitBreakerOpenError) {
* // Circuit is open, handle gracefully
* }
* }
* ```
*/
export class CircuitBreaker {
private config: CircuitBreakerConfig;
private state: CircuitState = CircuitState.CLOSED;
private failures: FailureRecord[] = [];
private successCount = 0;
private openedAt?: number;
private nextAttemptTime?: number;
constructor(
private name: string,
config: Partial<CircuitBreakerConfig> = {}
) {
this.config = { ...DEFAULT_CIRCUIT_BREAKER_CONFIG, ...config };
}
/**
* Execute an operation through the circuit breaker
*/
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (!this.config.enabled) {
return operation();
}
// Check circuit state
this.updateState();
if (this.state === CircuitState.OPEN) {
const nextAttempt = this.nextAttemptTime
? new Date(this.nextAttemptTime)
: new Date(Date.now() + this.config.timeout);
throw new CircuitBreakerOpenError(this.name, nextAttempt);
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure(error as Error);
throw error;
}
}
/**
* Handle successful operation
*/
private onSuccess(): void {
this.removeOldFailures();
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= this.config.successThreshold) {
this.transitionTo(CircuitState.CLOSED);
}
}
}
/**
* Handle failed operation
*/
private onFailure(error: Error): void {
this.failures.push({
timestamp: Date.now(),
error,
});
this.removeOldFailures();
if (this.state === CircuitState.HALF_OPEN) {
// Any failure in half-open state opens the circuit immediately
this.transitionTo(CircuitState.OPEN);
} else if (this.state === CircuitState.CLOSED) {
// Check if we've exceeded failure threshold
if (this.failures.length >= this.config.failureThreshold) {
this.transitionTo(CircuitState.OPEN);
}
}
}
/**
* Update circuit state based on time
*/
private updateState(): void {
if (this.state === CircuitState.OPEN && this.nextAttemptTime) {
if (Date.now() >= this.nextAttemptTime) {
this.transitionTo(CircuitState.HALF_OPEN);
}
}
}
/**
* Transition to a new state
*/
private transitionTo(newState: CircuitState): void {
const previousState = this.state;
this.state = newState;
switch (newState) {
case CircuitState.OPEN:
this.openedAt = Date.now();
this.nextAttemptTime = Date.now() + this.config.timeout;
this.successCount = 0;
break;
case CircuitState.HALF_OPEN:
this.successCount = 0;
break;
case CircuitState.CLOSED:
this.failures = [];
this.successCount = 0;
this.openedAt = undefined;
this.nextAttemptTime = undefined;
break;
}
if (previousState !== newState) {
this.onStateChange(previousState, newState);
}
}
/**
* Remove failures outside the rolling window
*/
private removeOldFailures(): void {
const cutoff = Date.now() - this.config.rollingWindow;
this.failures = this.failures.filter((f) => f.timestamp >= cutoff);
}
/**
* Callback when state changes (can be overridden)
*/
protected onStateChange(from: CircuitState, to: CircuitState): void {
// Override in subclass or use getState() to monitor
console.log(`Circuit breaker "${this.name}" transitioned from ${from} to ${to}`);
}
/**
* Get current circuit state
*/
getState(): CircuitState {
this.updateState();
return this.state;
}
/**
* Get circuit statistics
*/
getStats(): {
state: CircuitState;
failureCount: number;
successCount: number;
openedAt?: Date;
nextAttemptTime?: Date;
} {
this.removeOldFailures();
this.updateState();
return {
state: this.state,
failureCount: this.failures.length,
successCount: this.successCount,
...(this.openedAt && { openedAt: new Date(this.openedAt) }),
...(this.nextAttemptTime && { nextAttemptTime: new Date(this.nextAttemptTime) }),
};
}
/**
* Manually open the circuit
*/
open(): void {
this.transitionTo(CircuitState.OPEN);
}
/**
* Manually close the circuit
*/
close(): void {
this.transitionTo(CircuitState.CLOSED);
}
/**
* Reset the circuit breaker
*/
reset(): void {
this.failures = [];
this.successCount = 0;
this.openedAt = undefined;
this.nextAttemptTime = undefined;
this.state = CircuitState.CLOSED;
}
/**
* Check if circuit is open
*/
isOpen(): boolean {
this.updateState();
return this.state === CircuitState.OPEN;
}
/**
* Check if circuit is closed
*/
isClosed(): boolean {
this.updateState();
return this.state === CircuitState.CLOSED;
}
/**
* Check if circuit is half-open
*/
isHalfOpen(): boolean {
this.updateState();
return this.state === CircuitState.HALF_OPEN;
}
}