perf(fallback): connect timeout breve + circuit breaker persistente (fast-fail)
Il gateway irraggiungibile costava ~30s x4 tentativi (fino a ~2 minuti) per ogni
chiamata: ora si distinguono i due casi e le chiamate successive sono immediate.
- due timeout separati: `connectTimeoutMs` (default 2500, connect+headers) e
`timeoutMs` (default 30000, budget per il body). Nessuna risposta entro il
primo = "gateway non raggiungibile"; body lento = "elaborazione lunga"
- circuit breaker persistente in ~/.local/share/pi-qmem/breaker.json
(env QMEM_BREAKER_FILE): fallimento definitivo (connessione rifiutata/DNS/
connect timeout) → nessun retry e apertura immediata per `breakerBaseMs`
(default 120000 = 2 min) con escalation fino a `breakerMaxMs` (10 min);
5xx/body lento sono ambigui → retry con Retry-After e apertura dopo
`breakerTripAfter` (default 2). Un successo lo richiude; cambiando `url` lo
stato riparte chiuso (endpoint-aware)
- con breaker aperto gatewayRequest ritorna in ~0 ms senza rete
(`gateway_unreachable`, `breaker_open`, `retry_in_ms`): i tool passano subito
al fallback locale e l'outbox accoda
- fix di due bug scoperti durante i test:
* `res.json().catch(() => ({}))` trasformava un body non completato in
"successo con dati vuoti" → l'agente vedeva "nessun risultato" invece del
fallback locale. Ora è `timeout_body` (fallimento, ambiguo)
* `submitOrQueue` passava un AbortSignal esterno, che con la nuova semantica
sarebbe stato letto come annullamento utente (eccezione invece di coda)
- messaggi dei tool con lo stato del breaker e come forzare un tentativo;
`details.breaker` per l'osservabilità
- comandi: `/qmem:local breaker [reset]` e `qmem-sqlite breaker [--reset]`;
lo stato compare in `/qmem:local status` e nella CLI
- budget interni per enrich/pull/flush (niente AbortSignal esterni)
Misure: connessione rifiutata → 4-8 ms (prima: 4 x 30 s); front che risponde
503 dopo ~40 s → 3,5 s alla prima chiamata, poi 0 ms di rete a breaker aperto;
server che accetta e non risponde → 708 ms (connect timeout); body lento →
1,2 s senza aprire il breaker; persistenza verificata fra processi distinti.
Test: scripts/test-local.mjs 38/38 (nuova fase dedicata al breaker).
This commit is contained in:
@@ -44,7 +44,12 @@ Config salvata in `~/.config/pi-qmem/config.json` (0600):
|
||||
"apiKey": "...",
|
||||
"localDbPath": "~/.local/share/pi-qmem/qmem.sqlite",
|
||||
"localFallback": true,
|
||||
"offlineQueue": true
|
||||
"offlineQueue": true,
|
||||
"connectTimeoutMs": 2500,
|
||||
"timeoutMs": 30000,
|
||||
"breakerBaseMs": 120000,
|
||||
"breakerMaxMs": 600000,
|
||||
"breakerTripAfter": 2
|
||||
}
|
||||
```
|
||||
|
||||
@@ -52,6 +57,30 @@ Config salvata in `~/.config/pi-qmem/config.json` (0600):
|
||||
disabilita l'accodamento offline in scrittura (lo store torna a fallire come
|
||||
prima).
|
||||
|
||||
## Circuit breaker (fast-fail quando il gateway è irraggiungibile)
|
||||
|
||||
Due timeout distinti, per non confondere "gateway giù" con "elaborazione lunga":
|
||||
|
||||
| Fase | Chiave | Default | Significato |
|
||||
|---|---|---|---|
|
||||
| **connect + headers** | `connectTimeoutMs` | **2500** | nessuna risposta entro questo tempo → **gateway non raggiungibile** (fallimento definitivo) |
|
||||
| **body** (dopo gli header) | `timeoutMs` | 30000 | budget per il rerank/export/ricerca: un superamento è un fallimento **ambiguo** |
|
||||
|
||||
Comportamento:
|
||||
|
||||
- **Connessione fallita/nessuna risposta** (ECONNREFUSED, DNS, timeout di connect): **nessun retry**, il
|
||||
**circuit breaker** si apre subito e resta aperto `breakerBaseMs` (**2 min**), con escalation
|
||||
esponenziale fino a `breakerMaxMs` (10 min).
|
||||
- **5xx o body lento**: fallimenti **ambigui** → retry con `Retry-After` e breaker solo dopo
|
||||
`breakerTripAfter` (default 2) fallimenti consecutivi.
|
||||
- **Breaker aperto**: le chiamate ritornano in **~0 ms senza toccare la rete** (`error:
|
||||
"gateway_unreachable"`, `breaker_open: true`, `retry_in_ms`), quindi i tool passano subito al
|
||||
fallback locale e l'outbox accoda senza attese.
|
||||
- **Stato persistente**: `~/.local/share/pi-qmem/breaker.json` (env `QMEM_BREAKER_FILE`) → vale anche
|
||||
per nuove sessioni, `/reload` e processi CLI. Cambiando `url` il breaker riparte chiuso.
|
||||
- **Reset manuale**: `/qmem:local breaker reset` oppure `node scripts/qmem-sqlite.mjs breaker --reset`;
|
||||
un successo lo richiude da solo. Stato: `/qmem:local breaker` o `qmem-sqlite breaker`.
|
||||
|
||||
## Indice locale (fallback offline)
|
||||
|
||||
Il gateway remoto non è sempre raggiungibile (VPN giù, nodi offline). L'estensione
|
||||
@@ -99,9 +128,13 @@ Comandi (TUI) e CLI standalone:
|
||||
node scripts/qmem-sqlite.mjs status|import|find "query"|enrich|pull
|
||||
node scripts/qmem-sqlite.mjs store --project P --text "..." [--queue-only]
|
||||
node scripts/qmem-sqlite.mjs queue|flush
|
||||
node scripts/test-local.mjs # suite di test (24 controlli, HOME temporanea)
|
||||
node scripts/test-local.mjs # suite di test (37 controlli, HOME temporanea)
|
||||
```
|
||||
|
||||
Nota: un body non completato **non** viene più restituito come "successo con dati vuoti"
|
||||
(prima `res.json().catch(() => ({}))` mascherava il timeout: l'agente vedeva "nessun risultato"
|
||||
invece del fallback locale).
|
||||
|
||||
Limiti dichiarati: è uno **storico osservato** (più vecchio del gateway), la
|
||||
ricerca è **lessicale** (nessuno score 0.45/0.60: non applicare le soglie
|
||||
semantiche) e un record in coda (⏳) **non è ancora nella memoria condivisa**:
|
||||
|
||||
Reference in New Issue
Block a user