Files
telegram-bot-workers/src/utils/circuit-breaker.ts
kappa c0e47482c4 feat(phase-5-3): 모니터링 강화
logger.ts, metrics.ts, /api/metrics 추가
Version: e3bcb4ae
2026-01-19 16:43:36 +09:00

261 lines
6.8 KiB
TypeScript

/**
* Circuit Breaker pattern implementation
*
* Prevents cascading failures by temporarily blocking requests
* to a failing service, giving it time to recover.
*
* @example
* ```typescript
* const breaker = new CircuitBreaker({ failureThreshold: 5 });
*
* try {
* const result = await breaker.execute(async () => {
* return await fetch('https://api.example.com');
* });
* } catch (error) {
* if (error instanceof CircuitBreakerError) {
* console.log('Circuit is open, service unavailable');
* }
* }
* ```
*/
/**
* Circuit breaker states
*/
export enum CircuitState {
/** Circuit is closed - requests pass through normally */
CLOSED = 'CLOSED',
/** Circuit is open - all requests are immediately rejected */
OPEN = 'OPEN',
/** Circuit is half-open - one test request is allowed */
HALF_OPEN = 'HALF_OPEN',
}
/**
* Configuration options for circuit breaker
*/
export interface CircuitBreakerOptions {
/** Number of consecutive failures before opening circuit (default: 5) */
failureThreshold?: number;
/** Time in ms to wait before attempting recovery (default: 60000) */
resetTimeoutMs?: number;
/** Time window in ms for monitoring failures (default: 120000) */
monitoringWindowMs?: number;
}
/**
* Custom error thrown when circuit is open
*/
export class CircuitBreakerError extends Error {
constructor(
message: string,
public readonly state: CircuitState
) {
super(message);
this.name = 'CircuitBreakerError';
}
}
/**
* Tracks failure events with timestamps
*/
interface FailureRecord {
timestamp: number;
error: Error;
}
/**
* Circuit Breaker implementation
*
* Monitors operation failures and automatically opens the circuit
* when failure threshold is exceeded, preventing further attempts
* until a reset timeout has elapsed.
*/
export class CircuitBreaker {
private state: CircuitState = CircuitState.CLOSED;
private failures: FailureRecord[] = [];
private openedAt: number | null = null;
private successCount = 0;
private failureCount = 0;
private readonly failureThreshold: number;
private readonly resetTimeoutMs: number;
private readonly monitoringWindowMs: number;
constructor(options?: CircuitBreakerOptions) {
this.failureThreshold = options?.failureThreshold ?? 5;
this.resetTimeoutMs = options?.resetTimeoutMs ?? 60000;
this.monitoringWindowMs = options?.monitoringWindowMs ?? 120000;
console.log('[CircuitBreaker] Initialized', {
failureThreshold: this.failureThreshold,
resetTimeoutMs: this.resetTimeoutMs,
monitoringWindowMs: this.monitoringWindowMs,
});
}
/**
* Get current circuit state
*/
getState(): CircuitState {
return this.state;
}
/**
* Get circuit statistics
*/
getStats() {
const lastFailure = this.failures.length > 0
? this.failures[this.failures.length - 1]
: null;
return {
state: this.state,
failures: this.failures.length,
lastFailureTime: lastFailure ? new Date(lastFailure.timestamp) : undefined,
stats: {
totalRequests: this.successCount + this.failureCount,
totalFailures: this.failureCount,
totalSuccesses: this.successCount,
},
config: {
failureThreshold: this.failureThreshold,
resetTimeoutMs: this.resetTimeoutMs,
monitoringWindowMs: this.monitoringWindowMs,
},
};
}
/**
* Manually reset the circuit to closed state
*/
reset(): void {
console.log('[CircuitBreaker] Manual reset');
this.state = CircuitState.CLOSED;
this.failures = [];
this.openedAt = null;
this.successCount = 0;
this.failureCount = 0;
}
/**
* Remove old failure records outside monitoring window
*/
private cleanupOldFailures(): void {
const now = Date.now();
const cutoff = now - this.monitoringWindowMs;
this.failures = this.failures.filter(
record => record.timestamp > cutoff
);
}
/**
* Check if circuit should transition to half-open state
*/
private checkResetTimeout(): void {
if (this.state === CircuitState.OPEN && this.openedAt !== null) {
const now = Date.now();
const elapsed = now - this.openedAt;
if (elapsed >= this.resetTimeoutMs) {
console.log('[CircuitBreaker] Reset timeout reached, transitioning to HALF_OPEN');
this.state = CircuitState.HALF_OPEN;
}
}
}
/**
* Record a successful operation
*/
private onSuccess(): void {
this.successCount++;
if (this.state === CircuitState.HALF_OPEN) {
console.log('[CircuitBreaker] Half-open test succeeded, closing circuit');
this.state = CircuitState.CLOSED;
this.failures = [];
this.openedAt = null;
}
}
/**
* Record a failed operation
*/
private onFailure(error: Error): void {
this.failureCount++;
const now = Date.now();
this.failures.push({ timestamp: now, error });
// Clean up old failures
this.cleanupOldFailures();
// If in half-open state, one failure reopens the circuit
if (this.state === CircuitState.HALF_OPEN) {
console.log('[CircuitBreaker] Half-open test failed, reopening circuit');
this.state = CircuitState.OPEN;
this.openedAt = now;
return;
}
// Check if we should open the circuit
if (this.state === CircuitState.CLOSED) {
if (this.failures.length >= this.failureThreshold) {
console.log(
`[CircuitBreaker] Failure threshold (${this.failureThreshold}) exceeded, opening circuit`
);
this.state = CircuitState.OPEN;
this.openedAt = now;
}
}
}
/**
* Execute a function through the circuit breaker
*
* @param fn - Async function to execute
* @returns Promise resolving to the function's result
* @throws CircuitBreakerError if circuit is open
* @throws Original error if function fails
*/
async execute<T>(fn: () => Promise<T>): Promise<T> {
// Check if we should transition to half-open
this.checkResetTimeout();
// If circuit is open, reject immediately
if (this.state === CircuitState.OPEN) {
const error = new CircuitBreakerError(
'Circuit breaker is open - service unavailable',
this.state
);
console.log('[CircuitBreaker] Request blocked - circuit is OPEN');
throw error;
}
try {
// Execute the function
const result = await fn();
// Record success
this.onSuccess();
return result;
} catch (error) {
// Record failure
const err = error instanceof Error ? error : new Error(String(error));
this.onFailure(err);
// Log failure
console.error(
`[CircuitBreaker] Operation failed (${this.failures.length}/${this.failureThreshold} failures):`,
err.message
);
// Re-throw the original error
throw err;
}
}
}