实测(2026-10-13,本机空闲,活库 cyrene/user 分区 201 条、均长 34.7 汉字): - 一条入库审查召回链 = 51 个「4 条」请求 = 23.070 / 21.860 s;旧审查预算 20s 比链长还短 ⇒ 每一条候选满额的审查都超时,且超时后 **一行留痕都不写** (7 天里 92.1% 的审查 recall_mode 为空、2274 条超时降级的根因) - 同一条链改成「8 条」= 26 个请求 = 21.435 / 21.597 s:请求数减半,墙钟几乎不变 —— 链路耗时由「条数 × 每条成本」主导,批次只改请求数 - 「16 条」在**真实代码路径**上 3 次里 2 次让单请求越过 5s 单次超时(1.818 / 5.016 超时 / 5.001 超时)⇒ 批上限不越过 8;隔离测量里 12 条 0.996~1.017 s 仍在界内 改动: - embedding: perItemBudget 500ms→300ms(实测摊销 70~107ms/条的约 3 倍) ⇒ DefaultBatchLimit 由 (5s/2−100ms)/300ms = **8**,同一套推演、两维都成立 (条数 100ms+8×300ms=2.5s;字符 100ms+480×5ms=2.5s,均在超时的一半内) - embedding: 新增 maxSafeBatchLimit=12(实测外沿)+ 编译期断言(越界即 uint 溢出、 编译失败),与 tidy_config.go 那条同手法 - memory: defaultReviewTimeout 20s→**90s**(> 实测最坏链:空闲 21.6s⇒4.2 倍余量、 与整理链并发 56.7s + T1 8s ⇒ 1.4 倍;不再大的理由=单 worker 有界队列 256 的 最坏积压 256×90s≈6.4h、服务真挂死时第一片 5s 就降级用不到这条预算) 测试(不留过时断言):budget_test.go 加「钉值 8/480 + 两维不等式 + ≤12 外沿 + 16 条必须仍判越界」四层;api_url_test.go 的 reviewBudgetRef 20s→90s; preinsert_recall_test.go 新增跨包断言「预算 > 实测最坏链 + defaultT1Timeout」。 变异自证:perItemBudget 回 500ms ⇒ 钉值断言红;reviewTimeout 回 20s ⇒ 跨包断言红; perItemBudget 150ms(⇒16)⇒ 编译失败(constant -4 overflows uint)。三者均已逐字还原。 遗留(如实登记,未修):① 整理轮每轮重嵌整个分区(user 一轮仍 250 个请求), 解法方向是复用已入库的 vec_items 或给整理轮一个 embedding 请求预算;② 链路对单点 慢分片没有韧性 —— 一个分片失败即整条 Embed 失败、丢掉其余分片的工作量,且实测 任何批次形状都会偶发 >5s 尖峰(4 条 7.139s / 8 条 9.711s,与批次无关),方向是 重试/分片级容错,调常量解决不了。
71 lines
3.8 KiB
Go
71 lines
3.8 KiB
Go
package memory
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/embedding"
|
||
)
|
||
|
||
// measuredReviewChain* 是**实测参照值**(2026-10-13,本机空闲;正文取自活库
|
||
// cyrene/user 分区 201 条,均长 34.7 汉字;完整曲线与复跑命令见
|
||
// internal/runtime/embedding/budget.go 的文件头)。它们只用于本文件的断言 ——
|
||
// 与 embedding 侧 api_url_test.go 的 reviewBudgetRef 同一手法:把「拿实测数字做的
|
||
// 决定」留成可复核的常量,而不是消失在注释里。
|
||
//
|
||
// 链的形状 = ⌈(ScanLimit + 1) / embedding.DefaultBatchLimit⌉ 个请求:
|
||
//
|
||
// 改前 4 条/片:51 个请求 ⇒ 墙钟 23.070 / 21.860 s
|
||
// 改后 8 条/片:26 个请求 ⇒ 墙钟 21.435 / 21.597 s
|
||
//
|
||
// loaded 那一档取自 M14 只读勘测报告(同一条链与整理链并发时测得 56.7s)——
|
||
// 它是按**改前**的 4 条/片形状测的;链路墙钟由「条数 × 每条成本」主导、与批次
|
||
// 形状几乎无关,因此拿它当改后代码的最坏参照是**偏保守**的一侧。
|
||
const (
|
||
measuredReviewChainIdle = 22 * time.Second
|
||
measuredReviewChainLoaded = 57 * time.Second
|
||
)
|
||
|
||
// TestEmbeddingTimeoutFitsReviewBudget 钉住两条**跨包**口径:
|
||
//
|
||
// 1. embedding 单次调用的独立超时必须严格小于入库前审查的单条总预算;
|
||
// 2. 反过来,审查的单条总预算必须严格大于「实测最坏召回链长 + T1 自己的超时」。
|
||
//
|
||
// 第 1 条的理由(2026-10-07 实测的故障形态):端点主机写成 localhost → 先试 IPv6
|
||
// ::1 → 连接等系统超时(Windows 上约 21s),一次 embedding 失败正好吃光预算,
|
||
// 整条审查超时(`memory/review: context deadline exceeded`),排在后面的 T1
|
||
// 灰区判定连执行机会都没有。地址规范化(embedding.normalizeEndpointURL)治的是根,
|
||
// 第 1 条守的是「就算再有新的卡死来源,也得先把预算留给后续步骤」。
|
||
//
|
||
// 第 2 条是本次修复(2026-10-13)的核心断言,**变异自证**:把 defaultReviewTimeout
|
||
// 调回 20s ⇒ 这一条必红。它守的是「预算 ≥ 链长 + T1」这条不可让步的下界 ——
|
||
// 链长随 ScanLimit / 批上限漂移,T1 超时随 defaultT1Timeout 漂移,任一侧动到把
|
||
// 预算吃掉都会被它抓到。旧值 20s 正是踩在这条线上的:实测链长 21~23s(空闲),
|
||
// 于是每一条候选满额的审查都必然超时,7 天里 2274 条审查因此降级为空 recall_mode。
|
||
//
|
||
// embedding 侧同一口径也有一条钉值测试(api_url_test.go),两边互为护栏。
|
||
func TestEmbeddingTimeoutFitsReviewBudget(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
if embedding.DefaultRequestTimeout <= 0 {
|
||
t.Fatalf("embedding.DefaultRequestTimeout = %s, 必须为正", embedding.DefaultRequestTimeout)
|
||
}
|
||
if embedding.DefaultRequestTimeout >= defaultReviewTimeout {
|
||
t.Fatalf("embedding 单次超时 %s 必须严格小于审查总预算 %s",
|
||
embedding.DefaultRequestTimeout, defaultReviewTimeout)
|
||
}
|
||
// 第 2 条:预算必须盖得住实测最坏链长,还要留得下 T1 那一步自己的超时
|
||
// (T1 在召回**之后**跑,同一条 ctx)。空闲余量倍数由 budget.go 的注释列明,
|
||
// 这里只钉住「盖得住」这条下界。
|
||
worst := measuredReviewChainLoaded + defaultT1Timeout
|
||
if defaultReviewTimeout <= worst {
|
||
t.Fatalf("审查单条预算 %s 必须严格大于「实测最坏召回链 %s + T1 超时 %s = %s」—— "+
|
||
"否则链还没跑完预算就用光(2026-10-13 的 2274 条超时降级正是这条)",
|
||
defaultReviewTimeout, measuredReviewChainLoaded, defaultT1Timeout, worst)
|
||
}
|
||
if measuredReviewChainIdle >= defaultReviewTimeout {
|
||
t.Fatalf("空闲实测链长 %s 已不小于审查预算 %s(余量必须为正)",
|
||
measuredReviewChainIdle, defaultReviewTimeout)
|
||
}
|
||
}
|