Files
ARTEX-chixuweihu/llmpool/health_policy_test.go
T
Autumn-27 a50bf02428 feat(llm): 五层重试的次数与间隔可配
一次模型调用的失败由内到外经过五层重试:建连(SDK)、空响应(SDK)、同 provider
安全窗口、轮询熔断、意图重跑。此前五层的次数与间隔全是写死的常量,遇到脾气各异
的中转站只能改代码。现在都能在 LLM 页配置,不配就是原来的默认值。

配置语义五层统一(db.RetryRule):次数 0=内置默认 / -1=关掉这层 / >0=该值;
间隔 0=原本的指数退避 / >0=改用固定间隔。0 被"未配置"占用,故关闭用 -1。
写库前夹紧到 次数 [-1,20]、间隔 [0,1h],DB 另有 CHECK 兜底。

粒度按层的性质分:前三层跟着端点走,可在每个模型配置里逐字段覆盖全局默认(只钉
间隔的仍继承全局次数);熔断与意图重跑是进程级语义,只有全局一份。

生效路径:①②是 provider 构建参数,保存后重建缓存;③随每次 hooks.current()
解析,故障转移换配置时重试节奏跟着换;④在失败路径上每次都要读,由 applyRetryPolicy
推给 llmpool.Registry;⑤每条意图收尾读一次。全部热生效,无需重启。

连接测试刻意不带这些参数——它有 30s 硬超时,叠上用户配的重试只会把本来能用的
端点测成超时失败。

norma 升 v0.3.5(llm.Config 新增 RetryInterval / EmptyResponseRetries /
EmptyResponseInterval)。旧库升级后行为逐字节不变:新列默认 0,settings 键不
存在即全默认。设计见 docs/LLM重试设计.md。
2026-09-09 13:35:04 -04:00

68 lines
2.2 KiB
Go

package llmpool
import (
"testing"
"time"
)
// A configured soft-trip threshold replaces the default: two transient failures
// are enough, and the fixed cooldown replaces the 1/5/30min ladder.
func TestSetPolicyOverridesThresholdAndCooldown(t *testing.T) {
reg := NewRegistry(nil, nil)
reg.SetPolicy(2, 90*time.Second)
if reg.Trip(1, "429", false) {
t.Fatal("tripped on the first transient failure, want the second")
}
if !reg.Trip(1, "429", false) {
t.Fatal("should trip on transient failure #2")
}
st := reg.Get(1)
if d := time.Until(st.OpenUntil); d < 80*time.Second || d > 90*time.Second {
t.Fatalf("cooldown=%v, want ~90s", d)
}
// Every later trip keeps the same fixed window instead of climbing the ladder.
reg.Trip(1, "429", true)
st = reg.Get(1)
if d := time.Until(st.OpenUntil); d < 80*time.Second || d > 90*time.Second {
t.Fatalf("second cooldown=%v, want ~90s (fixed)", d)
}
}
// A negative threshold turns off soft tripping entirely: transient failures never
// open the breaker, deterministic ones still do immediately.
func TestSetPolicyDisablesSoftTrip(t *testing.T) {
reg := NewRegistry(nil, nil)
reg.SetPolicy(-1, 0)
for i := range 10 {
if reg.Trip(1, "429", false) {
t.Fatalf("transient failure #%d tripped the breaker, want never", i+1)
}
}
if reg.IsOpen(1) {
t.Fatal("breaker should stay closed for transient failures")
}
if !reg.Trip(1, "no credit", true) {
t.Fatal("a hard failure must still trip immediately")
}
if d := time.Until(reg.Get(1).OpenUntil); d < 50*time.Second || d > 60*time.Second {
t.Fatalf("cooldown=%v, want the default first rung (~1min)", d)
}
}
// The zero policy is the historical behaviour: 3 transient failures, ladder cooldown.
func TestZeroPolicyKeepsDefaults(t *testing.T) {
reg := NewRegistry(nil, nil)
reg.SetPolicy(0, 0)
for i := 1; i < softTripAfter; i++ {
if reg.Trip(1, "429", false) {
t.Fatalf("tripped after %d transient failures, want %d", i, softTripAfter)
}
}
if !reg.Trip(1, "429", false) {
t.Fatalf("should trip on failure #%d", softTripAfter)
}
if d := time.Until(reg.Get(1).OpenUntil); d < 50*time.Second || d > 60*time.Second {
t.Fatalf("cooldown=%v, want the first ladder rung (~1min)", d)
}
}