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