Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chronicle/common/test/circuitBreaker.spec.ts
13405 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
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
import { CircuitBreaker, CircuitState } from '../circuitBreaker';
8
9
describe('CircuitBreaker', () => {
10
beforeEach(() => {
11
vi.useFakeTimers();
12
});
13
14
afterEach(() => {
15
vi.useRealTimers();
16
});
17
18
it('starts in CLOSED state', () => {
19
const cb = new CircuitBreaker();
20
expect(cb.getState()).toBe(CircuitState.CLOSED);
21
expect(cb.canRequest()).toBe(true);
22
});
23
24
it('stays CLOSED below failure threshold', () => {
25
const cb = new CircuitBreaker({ failureThreshold: 5 });
26
for (let i = 0; i < 4; i++) {
27
cb.recordFailure();
28
}
29
expect(cb.getState()).toBe(CircuitState.CLOSED);
30
expect(cb.canRequest()).toBe(true);
31
});
32
33
it('opens after reaching failure threshold', () => {
34
const cb = new CircuitBreaker({ failureThreshold: 3 });
35
cb.recordFailure();
36
cb.recordFailure();
37
cb.recordFailure();
38
expect(cb.getState()).toBe(CircuitState.OPEN);
39
expect(cb.canRequest()).toBe(false);
40
});
41
42
it('transitions from OPEN to HALF_OPEN after reset timeout', () => {
43
const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 10 });
44
cb.recordFailure();
45
expect(cb.getState()).toBe(CircuitState.OPEN);
46
47
vi.advanceTimersByTime(10);
48
expect(cb.getState()).toBe(CircuitState.HALF_OPEN);
49
});
50
51
it('allows one probe in HALF_OPEN state', () => {
52
const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 0 });
53
cb.recordFailure();
54
55
// Should be HALF_OPEN immediately with resetTimeoutMs=0
56
expect(cb.getState()).toBe(CircuitState.HALF_OPEN);
57
expect(cb.canRequest()).toBe(true); // first probe
58
expect(cb.canRequest()).toBe(false); // second probe blocked
59
});
60
61
it('closes on success after HALF_OPEN probe', () => {
62
const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 0 });
63
cb.recordFailure();
64
65
expect(cb.canRequest()).toBe(true); // probe
66
cb.recordSuccess();
67
expect(cb.getState()).toBe(CircuitState.CLOSED);
68
expect(cb.getFailureCount()).toBe(0);
69
});
70
71
it('re-opens on failure during HALF_OPEN probe', () => {
72
const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 10 });
73
cb.recordFailure();
74
expect(cb.getState()).toBe(CircuitState.OPEN);
75
76
vi.advanceTimersByTime(10);
77
expect(cb.getState()).toBe(CircuitState.HALF_OPEN);
78
cb.canRequest(); // take probe
79
cb.recordFailure(); // probe fails → back to OPEN
80
expect(cb.getState()).toBe(CircuitState.OPEN);
81
});
82
83
it('resets to CLOSED state', () => {
84
const cb = new CircuitBreaker({ failureThreshold: 1 });
85
cb.recordFailure();
86
expect(cb.getState()).toBe(CircuitState.OPEN);
87
88
cb.reset();
89
expect(cb.getState()).toBe(CircuitState.CLOSED);
90
expect(cb.getFailureCount()).toBe(0);
91
expect(cb.canRequest()).toBe(true);
92
});
93
94
it('resets failure count on success', () => {
95
const cb = new CircuitBreaker({ failureThreshold: 5 });
96
cb.recordFailure();
97
cb.recordFailure();
98
expect(cb.getFailureCount()).toBe(2);
99
100
cb.recordSuccess();
101
expect(cb.getFailureCount()).toBe(0);
102
});
103
104
it('applies exponential backoff on repeated HALF_OPEN failures', () => {
105
const cb = new CircuitBreaker({
106
failureThreshold: 1,
107
resetTimeoutMs: 10,
108
maxResetTimeoutMs: 100,
109
probeTimeoutMs: 5,
110
});
111
112
// First failure → OPEN
113
cb.recordFailure();
114
expect(cb.getState()).toBe(CircuitState.OPEN);
115
116
// Advance past first reset timeout (10ms)
117
vi.advanceTimersByTime(10);
118
expect(cb.getState()).toBe(CircuitState.HALF_OPEN);
119
cb.canRequest(); // take the probe
120
cb.recordFailure(); // probe fails → back to OPEN
121
122
expect(cb.getState()).toBe(CircuitState.OPEN);
123
// Now timeout should be 20ms (doubled)
124
125
// Advance 15ms — should still be OPEN (timeout is now 20ms)
126
vi.advanceTimersByTime(15);
127
expect(cb.getState()).toBe(CircuitState.OPEN);
128
129
// Advance another 5ms — now past 20ms, should be HALF_OPEN
130
vi.advanceTimersByTime(5);
131
expect(cb.getState()).toBe(CircuitState.HALF_OPEN);
132
});
133
134
it('probe timeout prevents permanent deadlock', () => {
135
const cb = new CircuitBreaker({
136
failureThreshold: 1,
137
resetTimeoutMs: 0,
138
probeTimeoutMs: 10,
139
});
140
cb.recordFailure();
141
142
// Take probe
143
expect(cb.canRequest()).toBe(true);
144
// Probe is in-flight, second request blocked
145
expect(cb.canRequest()).toBe(false);
146
147
// Advance past probe timeout
148
vi.advanceTimersByTime(10);
149
// Probe timed out, should allow another
150
expect(cb.canRequest()).toBe(true);
151
});
152
});
153
154