Skip to content

Commit 29fc469

Browse files
authored
fix: preserve circuit breaker recovery timeout (#469) (#667)
1 parent 818c954 commit 29fc469

7 files changed

Lines changed: 90 additions & 11 deletions

File tree

.aiox-core/core/ids/circuit-breaker.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ class CircuitBreaker {
9494
*/
9595
recordFailure() {
9696
this._failureCount++;
97+
98+
if (this._state === STATE_OPEN) {
99+
return;
100+
}
101+
97102
this._lastFailureTime = Date.now();
98103

99104
if (this._state === STATE_HALF_OPEN) {

.aiox-core/data/entity-registry.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
metadata:
22
version: 1.0.0
3-
lastUpdated: '2026-05-07T10:44:44.077Z'
3+
lastUpdated: '2026-05-07T13:24:28.327Z'
44
entityCount: 746
55
checksumAlgorithm: sha256
66
resolutionRate: 100
@@ -9053,8 +9053,8 @@ entities:
90539053
score: 0.4
90549054
constraints: []
90559055
extensionPoints: []
9056-
checksum: sha256:1b35331ba71a6ce17869bab255e087fc540291243f9884fc21ed89f7efc122a4
9057-
lastVerified: '2026-03-11T00:48:55.879Z'
9056+
checksum: sha256:63be126f2e0d320daa60cb5b68b21df93cad4c19d1f959e06a6ac776213f8eba
9057+
lastVerified: '2026-05-07T13:24:28.323Z'
90589058
framework-governor:
90599059
path: .aiox-core/core/ids/framework-governor.js
90609060
layer: L1

.aiox-core/install-manifest.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
# - SHA256 hashes for change detection
88
# - File types for categorization
99
#
10-
version: 5.1.6
11-
generated_at: "2026-05-07T12:43:37.364Z"
10+
version: 5.1.7
11+
generated_at: "2026-05-07T13:26:26.223Z"
1212
generator: scripts/generate-install-manifest.js
1313
file_count: 1103
1414
files:
@@ -697,9 +697,9 @@ files:
697697
type: core
698698
size: 22865
699699
- path: core/ids/circuit-breaker.js
700-
hash: sha256:1b35331ba71a6ce17869bab255e087fc540291243f9884fc21ed89f7efc122a4
700+
hash: sha256:63be126f2e0d320daa60cb5b68b21df93cad4c19d1f959e06a6ac776213f8eba
701701
type: core
702-
size: 4235
702+
size: 4295
703703
- path: core/ids/framework-governor.js
704704
hash: sha256:ef7a3b7444a51f2f122c114c662b1db544d1c9e7833dc6f77dce30ac3a2005a2
705705
type: core
@@ -1229,7 +1229,7 @@ files:
12291229
type: data
12301230
size: 9590
12311231
- path: data/entity-registry.yaml
1232-
hash: sha256:b79e4aa7eece851a30ad7b3598efc3033fd921f5b0b19dac5843a454ee8eec98
1232+
hash: sha256:39bacf2f05602f8cfa709b67062d5c9bfc1cac232ee68038e14f711ce060499a
12331233
type: data
12341234
size: 522429
12351235
- path: data/learned-patterns.yaml
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# STORY-123.11: Preservar timeout de recuperação do CircuitBreaker em OPEN
2+
3+
Status: Done
4+
5+
Issue: #469
6+
PR relacionado: #618
7+
8+
## Contexto
9+
10+
O issue #469 descreve um bug no `CircuitBreaker`: chamadas repetidas a `recordFailure()` enquanto o circuito já está `OPEN` atualizam `lastFailureTime`, reiniciando indefinidamente a janela de recuperação. Isso impede a transição para `HALF_OPEN` enquanto as falhas continuam chegando.
11+
12+
## Acceptance Criteria
13+
14+
- [x] AC1. Falhas registradas enquanto o circuito está `OPEN` continuam incrementando `failureCount`.
15+
- [x] AC2. Falhas registradas enquanto o circuito está `OPEN` não atualizam `lastFailureTime`.
16+
- [x] AC3. O circuito transiciona para `HALF_OPEN` após o timeout original, mesmo com falhas subsequentes durante `OPEN`.
17+
- [x] AC4. Há teste automatizado determinístico cobrindo a regressão do issue #469.
18+
- [x] AC5. A correção é publicada como patch `5.1.7`.
19+
20+
## Tasks
21+
22+
- [x] Ajustar `recordFailure()` para preservar o marco temporal original quando o estado já é `OPEN`.
23+
- [x] Adicionar regressão na suíte de verification gates.
24+
- [x] Atualizar versão e manifesto de instalação.
25+
- [x] Rodar gates locais antes do PR.
26+
27+
## Dev Notes
28+
29+
- O PR #618 continha a intenção correta, mas estava atrás da `main` e sem a bateria atual de validação; a correção foi reaplicada em branch nova baseada na `main`.
30+
- O comportamento em `CLOSED` e `HALF_OPEN` permanece igual: falhas nesses estados ainda atualizam `lastFailureTime`.
31+
32+
## File List
33+
34+
- [docs/stories/epic-123/STORY-123.11-circuit-breaker-recovery-timeout.md](./STORY-123.11-circuit-breaker-recovery-timeout.md)
35+
- [.aiox-core/core/ids/circuit-breaker.js](../../../.aiox-core/core/ids/circuit-breaker.js)
36+
- [tests/core/ids/verification-gates.test.js](../../../tests/core/ids/verification-gates.test.js)
37+
- [.aiox-core/install-manifest.yaml](../../../.aiox-core/install-manifest.yaml)
38+
- [package.json](../../../package.json)
39+
- [package-lock.json](../../../package-lock.json)

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aiox-squads/core",
3-
"version": "5.1.6",
3+
"version": "5.1.7",
44
"description": "Synkra AIOX: AI-Orchestrated System for Full Stack Development - Core Framework",
55
"bin": {
66
"aiox": "bin/aiox.js",

tests/core/ids/verification-gates.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,41 @@ describe('CircuitBreaker', () => {
189189
expect(cb.getState()).toBe(STATE_HALF_OPEN);
190190
});
191191

192+
it('preserves the original recovery timeout when failures continue while open', () => {
193+
let now = 1000;
194+
const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => now);
195+
196+
try {
197+
const breaker = new CircuitBreaker({
198+
failureThreshold: 2,
199+
successThreshold: 1,
200+
resetTimeoutMs: 1000,
201+
});
202+
203+
breaker.recordFailure();
204+
now = 1100;
205+
breaker.recordFailure();
206+
207+
expect(breaker.getState()).toBe(STATE_OPEN);
208+
expect(breaker.getStats().lastFailureTime).toBe(1100);
209+
210+
now = 1500;
211+
breaker.recordFailure();
212+
213+
expect(breaker.getStats().failureCount).toBe(3);
214+
expect(breaker.getStats().lastFailureTime).toBe(1100);
215+
216+
now = 2099;
217+
expect(breaker.isAllowed()).toBe(false);
218+
219+
now = 2100;
220+
expect(breaker.isAllowed()).toBe(true);
221+
expect(breaker.getState()).toBe(STATE_HALF_OPEN);
222+
} finally {
223+
nowSpy.mockRestore();
224+
}
225+
});
226+
192227
it('reports correct stats', () => {
193228
const stats = cb.getStats();
194229
expect(stats.state).toBe(STATE_OPEN);

0 commit comments

Comments
 (0)