BREAKING CHANGE(core): Refactor to v3: introduce modular core/domain architecture, plugin system, observability and strict TypeScript configuration; remove legacy classes
This commit is contained in:
306
ts/core/connection/circuit-breaker.ts
Normal file
306
ts/core/connection/circuit-breaker.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user