learning_ai_common_plat/services/extraction-service/src/lib/circuit-breaker.ts
saravanakumardb1 b8c0a73e89 feat(extraction): Phase 5 observability + error handling (5.7-5.12)
- 5.7: Enhanced structured logging with userId, productId, cacheHit, tokenCount
- 5.8: Metrics module (counters + histograms) + /extract/metrics endpoint
- 5.9: Grafana dashboard config for extraction-service (Loki queries)
- 5.10: Error mapping — sidecar errors → proper HTTP status codes (408, 429, 502, 503)
- 5.11: Circuit breaker for Python sidecar (5 failures → 30s OPEN)
- 5.12: Graceful degradation — circuit open returns 503, cached results still served
- 46 TS tests passing
2026-02-14 14:04:59 -08:00

86 lines
2.1 KiB
TypeScript

/**
* Simple circuit breaker for the Python sidecar.
*
* States: CLOSED (normal) → OPEN (fail fast) → HALF_OPEN (probe)
* Opens after `failureThreshold` consecutive failures.
* Resets after `resetTimeoutMs` in OPEN state.
*/
type State = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
export interface CircuitBreakerOptions {
failureThreshold?: number;
resetTimeoutMs?: number;
}
export class CircuitBreaker {
private state: State = 'CLOSED';
private failureCount = 0;
private lastFailureTime = 0;
private readonly failureThreshold: number;
private readonly resetTimeoutMs: number;
constructor(opts: CircuitBreakerOptions = {}) {
this.failureThreshold = opts.failureThreshold ?? 5;
this.resetTimeoutMs = opts.resetTimeoutMs ?? 30_000; // 30s
}
/**
* Check if request is allowed. Throws if circuit is OPEN.
*/
allowRequest(): boolean {
if (this.state === 'CLOSED') return true;
if (this.state === 'OPEN') {
// Check if reset timeout has elapsed
if (Date.now() - this.lastFailureTime >= this.resetTimeoutMs) {
this.state = 'HALF_OPEN';
return true; // Allow one probe request
}
return false; // Still open, fail fast
}
// HALF_OPEN: allow the probe
return true;
}
/**
* Record a successful call — resets the breaker to CLOSED.
*/
recordSuccess(): void {
this.failureCount = 0;
this.state = 'CLOSED';
}
/**
* Record a failure — may trip the breaker to OPEN.
*/
recordFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
}
}
get currentState(): State {
return this.state;
}
get stats(): { state: State; failureCount: number; threshold: number; resetMs: number } {
return {
state: this.state,
failureCount: this.failureCount,
threshold: this.failureThreshold,
resetMs: this.resetTimeoutMs,
};
}
}
// Module-level singleton for the sidecar circuit breaker
export const sidecarBreaker = new CircuitBreaker({
failureThreshold: 5,
resetTimeoutMs: 30_000,
});