* feat(health): 支持凭据按模型冷却与统一恢复 * fix(health): 修复模型冷却的错误契约与恢复边界 * fix(web): 修复模型冷却展示与重置反馈 * fix(web): 保持分组健康与恢复反馈一致 * fix(health): 修复停用分组的模型冷却身份展示
82 lines
2.9 KiB
Go
82 lines
2.9 KiB
Go
package state
|
|
|
|
import (
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
// CredentialRuntimeCheckpoint contains only the mutable health state that is safe to
|
|
// carry across a process restart. Persisted key configuration remains owned by
|
|
// SQLite and is matched by ID plus group ID during restore.
|
|
type CredentialRuntimeCheckpoint struct {
|
|
ID uint `json:"id"`
|
|
GroupID uint `json:"group_id"`
|
|
CooldownUntil time.Time `json:"cooldown_until"`
|
|
Blacklisted bool `json:"blacklisted"`
|
|
FailureCount int `json:"failure_count"`
|
|
IdentityGeneration uint64 `json:"identity_generation,omitempty"`
|
|
ModelCooldowns map[string]time.Time `json:"model_cooldowns,omitempty"`
|
|
}
|
|
|
|
// CaptureRuntimeCheckpoint returns detached runtime health state in stable
|
|
// order. It intentionally excludes credentials and failure generations.
|
|
func (r *CredentialRegistry) CaptureRuntimeCheckpoint() []CredentialRuntimeCheckpoint {
|
|
r.ExpireModelCooldowns(time.Now())
|
|
r.mu.RLock()
|
|
checkpoints := make([]CredentialRuntimeCheckpoint, 0, len(r.credentialGroups))
|
|
for _, bucket := range r.buckets {
|
|
for _, entry := range bucket {
|
|
checkpoints = append(checkpoints, CredentialRuntimeCheckpoint{
|
|
ID: entry.ID,
|
|
GroupID: entry.GroupID,
|
|
CooldownUntil: entry.CooldownUntil,
|
|
Blacklisted: entry.Blacklisted,
|
|
FailureCount: entry.FailureCount,
|
|
IdentityGeneration: entry.IdentityGeneration,
|
|
ModelCooldowns: cloneModelCooldowns(entry.ModelCooldowns),
|
|
})
|
|
}
|
|
}
|
|
r.mu.RUnlock()
|
|
sort.Slice(checkpoints, func(i, j int) bool {
|
|
if checkpoints[i].GroupID != checkpoints[j].GroupID {
|
|
return checkpoints[i].GroupID < checkpoints[j].GroupID
|
|
}
|
|
return checkpoints[i].ID < checkpoints[j].ID
|
|
})
|
|
return checkpoints
|
|
}
|
|
|
|
// RestoreRuntimeCheckpoint applies matching runtime health state and skips
|
|
// keys that were removed or moved to another group while the process was down.
|
|
func (r *CredentialRegistry) RestoreRuntimeCheckpoint(checkpoints []CredentialRuntimeCheckpoint) int {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
restored := 0
|
|
for _, checkpoint := range checkpoints {
|
|
if checkpoint.ID == 0 || checkpoint.GroupID == 0 ||
|
|
checkpoint.FailureCount < 0 {
|
|
continue
|
|
}
|
|
groupID, ok := r.credentialGroups[checkpoint.ID]
|
|
if !ok || groupID != checkpoint.GroupID {
|
|
continue
|
|
}
|
|
entry, ok := r.buckets[groupID][checkpoint.ID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
entry.CooldownUntil = checkpoint.CooldownUntil
|
|
entry.Blacklisted = checkpoint.Blacklisted
|
|
entry.FailureCount = checkpoint.FailureCount
|
|
if checkpoint.IdentityGeneration == entry.IdentityGeneration {
|
|
entry.ModelCooldowns = cloneModelCooldowns(checkpoint.ModelCooldowns)
|
|
pruneModelCooldowns(entry.ModelCooldowns, time.Now())
|
|
}
|
|
r.scheduling.SyncCredential(runtimeView(entry))
|
|
restored++
|
|
}
|
|
return restored
|
|
}
|