Files

146 lines
3.8 KiB
Go

package dispatchadapter
import (
"sync"
"time"
"github.com/qianfree/team-api/relaykit/dispatch"
)
// probeKey 本地探测限流的复合键:model 空串 = 渠道级,非空 = 渠道×模型级。
type probeKey struct {
ch int64
model string
}
// localState Redis 故障时的实例本地降级镜像(基线方案 §13):
// 保护仍在、只是不再全局协同。Redis 恢复后自然弃用(无需回填)。
type localState struct {
mu sync.Mutex
probeMs map[probeKey]int64 // 渠道/渠道×模型 → 最近探测放行时间(本地探测限流)
leases map[int64]map[string]int64 // channelID → requestID → 过期时间戳 ms(严格容量渠道)
credCD map[int64]time.Time // keyID → 冷却截止时间
health map[int64]*localHealth // channelID → 本地健康/熔断镜像(实例级熔断兜底)
}
// localHealth 实例本地的简化健康镜像:只维持连续失败熔断兜底,不做完整 EWMA。
type localHealth struct {
consecutiveFails int
}
func newLocalState() *localState {
return &localState{
probeMs: make(map[probeKey]int64),
leases: make(map[int64]map[string]int64),
credCD: make(map[int64]time.Time),
health: make(map[int64]*localHealth),
}
}
// tryProbe 本地探测限流:每窗口本实例只放行一个(失去全局协同,仍防打爆)。
// model 语义与 TryProbeToken 一致:空串 = 渠道级,非空 = 渠道×模型级。
func (l *localState) tryProbe(channelID int64, model string, windowMs int64) bool {
l.mu.Lock()
defer l.mu.Unlock()
now := time.Now().UnixMilli()
k := probeKey{ch: channelID, model: model}
if now-l.probeMs[k] < windowMs {
return false
}
l.probeMs[k] = now
return true
}
// acquireLease 严格容量渠道的本地租约(修订 R4:实例级保守限额,fail-closed)。
func (l *localState) acquireLease(channelID int64, requestID string, limit int, leaseMs int64) bool {
if limit <= 0 {
return false // 未配置上限的严格渠道降级期间直接拒绝
}
l.mu.Lock()
defer l.mu.Unlock()
now := time.Now().UnixMilli()
m, ok := l.leases[channelID]
if !ok {
m = make(map[string]int64)
l.leases[channelID] = m
}
// 清过期
for id, exp := range m {
if exp <= now {
delete(m, id)
}
}
if len(m) >= limit {
return false
}
m[requestID] = now + leaseMs
return true
}
func (l *localState) refreshLease(channelID int64, requestID string, leaseMs int64) {
l.mu.Lock()
defer l.mu.Unlock()
if m, ok := l.leases[channelID]; ok {
if _, held := m[requestID]; held {
m[requestID] = time.Now().UnixMilli() + leaseMs
}
}
}
func (l *localState) releaseLease(channelID int64, requestID string) {
l.mu.Lock()
defer l.mu.Unlock()
if m, ok := l.leases[channelID]; ok {
delete(m, requestID)
}
}
// coolCred 本地凭证冷却镜像(Redis 写失败时仍在本实例生效)。
func (l *localState) coolCred(keyID int64, ttl time.Duration) {
l.mu.Lock()
defer l.mu.Unlock()
l.credCD[keyID] = time.Now().Add(ttl)
}
// clearCred 解除本地凭证冷却镜像(更换 Key / 管理员重置时调用)。
func (l *localState) clearCred(keyID int64) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.credCD, keyID)
}
func (l *localState) isCredCooled(keyID int64) bool {
l.mu.Lock()
defer l.mu.Unlock()
until, ok := l.credCD[keyID]
if !ok {
return false
}
if time.Now().After(until) {
delete(l.credCD, keyID)
return false
}
return true
}
// observe Redis 不可用时的本地健康记账:只做连续失败计数,
// 供实例级熔断兜底判断(完整 EWMA 在 Redis 恢复后继续)。
func (l *localState) observe(o dispatch.Outcome) {
l.mu.Lock()
defer l.mu.Unlock()
h, ok := l.health[o.ChannelID]
if !ok {
h = &localHealth{}
l.health[o.ChannelID] = h
}
if o.Success {
h.consecutiveFails = 0
return
}
h.consecutiveFails++
}