- 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
86 lines
2.1 KiB
TypeScript
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,
|
|
});
|