Path: blob/main/extensions/copilot/src/extension/chronicle/common/circuitBreaker.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45/**6* Circuit breaker states:7* - CLOSED: Normal operation, requests are allowed8* - OPEN: Circuit is tripped, requests fail immediately9* - HALF_OPEN: Testing if the service has recovered10*/11export const CircuitState = {12CLOSED: 'CLOSED',13OPEN: 'OPEN',14HALF_OPEN: 'HALF_OPEN',15} as const;1617export type CircuitState = (typeof CircuitState)[keyof typeof CircuitState];1819export interface CircuitBreakerOptions {20/** Number of consecutive failures before opening the circuit. */21failureThreshold: number;22/** Time in milliseconds before attempting to close the circuit. */23resetTimeoutMs: number;24/** Time in milliseconds before a probe request times out and allows another probe. */25probeTimeoutMs: number;26/** Maximum reset timeout after exponential backoff on failed probes. Defaults to resetTimeoutMs (no backoff). */27maxResetTimeoutMs?: number;28}2930const DEFAULT_OPTIONS: CircuitBreakerOptions = {31failureThreshold: 5,32resetTimeoutMs: 1_000,33probeTimeoutMs: 30_000,34};3536/**37* Circuit breaker implementation to prevent cascading failures.38*39* When multiple consecutive failures occur, the circuit "opens" and40* immediately rejects subsequent requests without attempting them.41* After a cooldown period, it allows a single "probe" request to test42* if the service has recovered.43*44* Ported from copilot-agent-runtime/src/helpers/circuit-breaker.ts.45*/46export class CircuitBreaker {47private state: CircuitState = CircuitState.CLOSED;48private failureCount: number = 0;49private lastFailureTime: number = 0;50private probeInFlight: boolean = false;51private probeStartTime: number = 0;52private readonly options: CircuitBreakerOptions;53/** Current reset timeout, increases via exponential backoff on failed probes. */54private currentResetTimeoutMs: number;5556constructor(options: Partial<CircuitBreakerOptions> = {}) {57this.options = { ...DEFAULT_OPTIONS, ...options };58this.currentResetTimeoutMs = this.options.resetTimeoutMs;59}6061/**62* Get the current state of the circuit breaker.63*/64getState(): CircuitState {65this.updateState();66return this.state;67}6869/**70* Check if requests should be allowed through.71* In HALF_OPEN state, only one probe request is allowed at a time.72*/73canRequest(): boolean {74this.updateState();7576switch (this.state) {77case CircuitState.CLOSED:78return true;79case CircuitState.HALF_OPEN:80// Check if probe has timed out — prevents permanent deadlock81if (this.probeInFlight) {82const probeElapsed = Date.now() - this.probeStartTime;83if (probeElapsed >= this.options.probeTimeoutMs) {84this.probeInFlight = false;85}86}87if (this.probeInFlight) {88return false;89}90this.probeInFlight = true;91this.probeStartTime = Date.now();92return true;93case CircuitState.OPEN:94return false;95}96}9798/**99* Record a successful request. Resets failure count and closes the circuit.100*/101recordSuccess(): void {102this.failureCount = 0;103this.probeInFlight = false;104this.currentResetTimeoutMs = this.options.resetTimeoutMs;105this.state = CircuitState.CLOSED;106}107108/**109* Record a failed request. May open the circuit if threshold is exceeded.110*/111recordFailure(): void {112const wasHalfOpen = this.state === CircuitState.HALF_OPEN;113this.failureCount++;114this.lastFailureTime = Date.now();115this.probeInFlight = false;116117if (this.failureCount >= this.options.failureThreshold) {118this.state = CircuitState.OPEN;119}120121// Exponential backoff: double the probe interval after each failed probe122if (wasHalfOpen && this.state === CircuitState.OPEN) {123const maxTimeout = this.options.maxResetTimeoutMs ?? this.options.resetTimeoutMs;124this.currentResetTimeoutMs = Math.min(this.currentResetTimeoutMs * 2, maxTimeout);125}126}127128/**129* Get the number of consecutive failures.130*/131getFailureCount(): number {132return this.failureCount;133}134135/**136* Force reset the circuit breaker to closed state.137*/138reset(): void {139this.state = CircuitState.CLOSED;140this.failureCount = 0;141this.lastFailureTime = 0;142this.probeInFlight = false;143this.probeStartTime = 0;144this.currentResetTimeoutMs = this.options.resetTimeoutMs;145}146147/**148* Update state based on timeout — transitions from OPEN to HALF_OPEN149* after the reset timeout has elapsed.150*/151private updateState(): void {152if (this.state === CircuitState.OPEN) {153const elapsed = Date.now() - this.lastFailureTime;154if (elapsed >= this.currentResetTimeoutMs) {155this.state = CircuitState.HALF_OPEN;156}157}158}159}160161162