ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
58 lines
2.2 KiB
Go
58 lines
2.2 KiB
Go
package db
|
|
|
|
import "time"
|
|
|
|
// LLM failover health: one row per profile tracking its circuit-breaker state.
|
|
// The authoritative copy lives in memory (llmpool.Registry); this table only
|
|
// survives a restart so a cooling-off window isn't silently reset by one.
|
|
|
|
// LLMHealth is one profile's circuit-breaker state.
|
|
type LLMHealth struct {
|
|
ProfileID int64 `json:"profile_id"`
|
|
Fails int `json:"fails"` // consecutive failures; cleared on success
|
|
Trips int `json:"trips"` // total trips, drives the backoff ladder
|
|
OpenUntil *time.Time `json:"open_until"` // nil/past = closed (healthy)
|
|
LastError string `json:"last_error"`
|
|
LastAt time.Time `json:"last_at"`
|
|
}
|
|
|
|
// LoadLLMHealth returns the profiles still in an UNEXPIRED cooling-off window.
|
|
// Expired rows are deliberately skipped: after a restart a profile that has
|
|
// finished cooling should be treated as healthy again and re-probed on its next
|
|
// call, not resurrected as broken.
|
|
func (d *DB) LoadLLMHealth() ([]LLMHealth, error) {
|
|
rows, err := d.Query(`SELECT profile_id,fails,trips,open_until,COALESCE(last_error,''),last_at
|
|
FROM llm_profile_health WHERE open_until IS NOT NULL AND open_until > now()`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []LLMHealth
|
|
for rows.Next() {
|
|
var h LLMHealth
|
|
if err := rows.Scan(&h.ProfileID, &h.Fails, &h.Trips, &h.OpenUntil, &h.LastError, &h.LastAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, h)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// SaveLLMHealth upserts one profile's circuit-breaker state.
|
|
func (d *DB) SaveLLMHealth(h LLMHealth) error {
|
|
_, err := d.Exec(`
|
|
INSERT INTO llm_profile_health(profile_id,fails,trips,open_until,last_error,last_at)
|
|
VALUES ($1,$2,$3,$4,$5,now())
|
|
ON CONFLICT (profile_id) DO UPDATE SET
|
|
fails=EXCLUDED.fails, trips=EXCLUDED.trips, open_until=EXCLUDED.open_until,
|
|
last_error=EXCLUDED.last_error, last_at=now()`,
|
|
h.ProfileID, h.Fails, h.Trips, h.OpenUntil, h.LastError)
|
|
return err
|
|
}
|
|
|
|
// ClearLLMHealth drops one profile's state (manual "recover now" from the UI).
|
|
func (d *DB) ClearLLMHealth(profileID int64) error {
|
|
_, err := d.Exec(`DELETE FROM llm_profile_health WHERE profile_id=$1`, profileID)
|
|
return err
|
|
}
|