File size: 5,569 Bytes
6d60378 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | package pool
import (
"sort"
"strings"
"time"
)
// ModelLockRow 单个 (域, 模型) 的锁池画像:这个模型在这个域还能不能选、被锁了几个号、
// 最早/全部解锁是什么时候。
//
// 与 ModelBlocked 的分工:ModelBlocked 回答「**这一个**模型此刻是不是全池被挡」
// (供请求失败时回 model_unavailable),本结构是它的**全清单**版本——把当前所有
// 有未过期模型级冷却的 (域, 模型) 一次性列出来,供 /status 与面板直接展示,
// 运维不必先失败一次才知道哪个模型不能用、还要等多久。
type ModelLockRow struct {
// Model 裸模型名(不含 cn:/global: 前缀;域由 Realm 字段表达)。
Model string `json:"model"`
// Realm 账号域(cn | global)。同名模型在两个域各自独立计数。
Realm string `json:"realm"`
// Total 该域**参与选号**的账号数(禁用/暂停号不计,与 ModelBlocked 同口径),
// 作为「可选 / 总数」的分母。
Total int `json:"total"`
// Servable 此刻真正能服务该模型的账号数(healthyForModel 且未在途占满)。
Servable int `json:"servable"`
// Locked 因该模型自身冷却被挡的账号数。
Locked int `json:"locked"`
// State 聚合状态:
// - locked :全部参与选号的账号都被该模型冷却挡住(换模型或等解锁才有用,
// 与 ModelBlocked 的 Blocked 同口径);
// - starved :此刻没有号能服务,但不是模型冷却造成的(账号级冷却/熔断/在途占满)
// ——等一下就会好,与模型无关;
// - partial :仍有号能服务该模型,只是部分号被锁。
State string `json:"state"`
// UnlockAt 最早解锁时刻:第一个被锁账号恢复的时刻(partial 下「何时值得重试」)。
UnlockAt time.Time `json:"unlock_at"`
// FullyUnlockAt 全部解锁时刻:最后一个被锁账号恢复的时刻。
FullyUnlockAt time.Time `json:"fully_unlock_at"`
// Reason 该模型被锁的原因(取最早解锁账号的上游原文)。
Reason string `json:"reason,omitempty"`
}
// ModelLockView 汇总当前所有「有未过期模型级限流」的 (域, 模型),按不可用优先排序。
//
// 为什么需要它:选号失败时上游只回 no_healthy_account(issue #102),ModelBlocked 虽
// 已把「号都在、只是都对这个模型关闭」从「池子真没号」里区分出来,但那是一次请求
// 失败后的单模型回答;运维仍看不到「此刻一共哪些模型不能用、分别还要锁多久」。
// 本视图把 (域, 模型) 维度聚合成一张清单补齐这一点。
//
// 口径与选号/ModelBlocked 严格一致,三处易错点:
// - 只看**参与选号**的账号:disabled / paused 号跳过——它们的不可用与模型无关,
// 计进来会把「模型被锁」和「号被停了」混为一谈;
// - 只看**真正拦路由**的冷却:AuditOnly 台账(无重置时间的 6004)不改变选号,
// 既不产生行也不计入 Locked;
// - 已过期的冷却不算锁。
//
// 只读:持 RLock 遍历,不改任何状态。无锁时返回 nil(JSON 里是 null,面板按空态渲染)。
func (p *Pool) ModelLockView() []ModelLockRow {
p.mu.RLock()
defer p.mu.RUnlock()
now := time.Now()
type agg struct {
locked int
unlockAt time.Time
fullyAt time.Time
reason string
}
rows := make(map[string]*agg)
routable := make(map[string]int) // realm → 参与选号的账号数
for _, e := range p.byUID {
realm := e.a.Realm()
if e.disabled || e.paused {
continue
}
routable[realm]++
for model, mc := range e.modelCooldowns {
if mc.AuditOnly || mc.Until.IsZero() || !now.Before(mc.Until) {
continue
}
key := realm + "\x1f" + model
a := rows[key]
if a == nil {
a = &agg{}
rows[key] = a
}
a.locked++
if a.unlockAt.IsZero() || mc.Until.Before(a.unlockAt) {
a.unlockAt = mc.Until
a.reason = mc.Reason
}
if a.fullyAt.IsZero() || mc.Until.After(a.fullyAt) {
a.fullyAt = mc.Until
}
}
}
if len(rows) == 0 {
return nil
}
out := make([]ModelLockRow, 0, len(rows))
for key, a := range rows {
sep := strings.IndexByte(key, '\x1f')
realm, model := key[:sep], key[sep+1:]
row := ModelLockRow{
Model: model,
Realm: realm,
Total: routable[realm],
Locked: a.locked,
UnlockAt: a.unlockAt,
FullyUnlockAt: a.fullyAt,
Reason: a.reason,
}
for _, e := range p.byUID {
if e.a.Realm() != realm {
continue
}
if e.healthyForModel(now, model) && !p.inFlightFull(e) {
row.Servable++
}
}
switch {
case row.Servable > 0:
row.State = "partial"
case row.Locked >= row.Total:
// 参与选号的号全被这个模型挡住 —— 与 ModelBlocked 的 Blocked 同义。
row.State = "locked"
default:
row.State = "starved"
}
out = append(out, row)
}
rank := func(state string) int {
switch state {
case "locked":
return 0
case "starved":
return 1
default:
return 2
}
}
sort.Slice(out, func(i, j int) bool {
if ri, rj := rank(out[i].State), rank(out[j].State); ri != rj {
return ri < rj
}
if out[i].Model != out[j].Model {
return out[i].Model < out[j].Model
}
return out[i].Realm < out[j].Realm
})
return out
}
|