From e6b6cd12e3ca424e0ff29a9524e6f4a7459a0d24 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Tue, 27 Oct 2026 13:04:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(memory):=20=E4=B8=AD=E5=BF=83=E5=90=91?= =?UTF-8?q?=E9=87=8F=E8=BF=87=E6=9C=9F=E6=94=B9=E4=B8=BA=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E8=8A=82=E6=B5=81=E9=87=8D=E5=BB=BA=EF=BC=88=E6=AD=A4=E5=89=8D?= =?UTF-8?q?=E6=B0=B8=E4=B9=85=E5=A4=B1=E6=95=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现场:'centroid invalidated: samples 1391 -> store 1877' 每轮召回刷一行。 过期分支只打印然后 return nil,false,nil,而重建入口只存在于三个 CLI 工具里 ⇒ 块数一旦越过 20% 线就永远过期,中心化校正永久关闭。 修法:过期时登记一次后台重建(每 fingerprint 10 分钟一次,延迟 2s 等读锁释放)。 不能在锁内就地重建 —— LoadCentroid 全程持 RLock,RebuildCentroid/SaveCentroid 需要写锁,就地调用必然自死锁(实测 internal/memory 测试 180s 超时)。 --- internal/memory/centroid_store.go | 49 ++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/internal/memory/centroid_store.go b/internal/memory/centroid_store.go index e378b1ed..e3b42c69 100644 --- a/internal/memory/centroid_store.go +++ b/internal/memory/centroid_store.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "sync" "time" ) @@ -187,8 +188,18 @@ func (g *GraphDB) LoadCentroid(fingerprint string, queryDim int) (*Centroid, boo delta = -delta } if delta > centroidRebuildRatio { - log.Printf("[graph] centroid invalidated: samples %d -> store %d (change %.0f%% > %.0f%%)", + // ★ 过期就**就地重建**(2026-10-27 现场):此前这里只打印一行然后 + // `return nil, false, nil`,而重建入口只存在于三个 CLI 工具里。 + // 结果:库里的带向量块数一旦越过 20% 线(实测 1391 → 1877), + // 该库就永远处于"过期"状态 —— 每轮召回都读同一个旧 samples、 + // 每次都判过期、每次都不重建,中心化校正永久关闭,且日志每轮刷一行。 + log.Printf("[graph] centroid stale: samples %d -> store %d (change %.0f%% > %.0f%%)", vecCount, curVectored, delta*100, centroidRebuildRatio*100) + // ★ 重建必须**离开锁之后**做(2026-10-27 实测踩坑): + // 本函数全程持有 g.mu.RLock,而 RebuildCentroid/SaveCentroid 需要写锁 + // ⇒ 就地重建必然自死锁(internal/memory 的测试 180s 超时,真机召回挂住)。 + // 所以这里只登记一次异步重建,本轮照旧返回"不用校正"。 + g.scheduleCentroidRebuild(fingerprint) return nil, false, nil } if !meta.Anomalous { @@ -299,6 +310,42 @@ func (g *GraphDB) RebuildCentroid(fingerprint string) (*Centroid, bool, error) { return c, c.Stats().Anomalous, nil } +// scheduleCentroidRebuild 在后台重建中心(每 fingerprint 至少间隔 +// centroidRebuildMinInterval)。 +// +// 为什么异步 + 节流: +// - 同步重建会与调用方的读锁冲突(见 LoadCentroid 里的注释); +// - 中心过期是**常态**(块数持续增长),每次召回都重建会把召回拖到秒级。 +// +// 召回失败方向是安全的:本轮拿不到中心 ⇒ 不做中心化校正,只是分数未去主轴。 +var ( + centroidRebuildMu sync.Mutex + centroidRebuiltAt = map[string]time.Time{} + centroidRebuildWait = 10 * time.Minute +) + +func (g *GraphDB) scheduleCentroidRebuild(fingerprint string) { + centroidRebuildMu.Lock() + if last, ok := centroidRebuiltAt[fingerprint]; ok && time.Since(last) < centroidRebuildWait { + centroidRebuildMu.Unlock() + return + } + centroidRebuiltAt[fingerprint] = time.Now() + centroidRebuildMu.Unlock() + + go func(fp string) { + // 等本轮召回把读锁放掉(LoadCentroid 用 defer 解锁)。 + time.Sleep(2 * time.Second) + c, anomalous, err := g.RebuildCentroid(fp) + if err != nil { + log.Printf("[graph] centroid rebuild failed, recall continues without centering: %v", err) + return + } + log.Printf("[graph] centroid rebuilt: dim=%d samples=%d anomalous=%v", + len(c.Mean()), c.Count, anomalous) + }(fingerprint) +} + // CentroidStatus 供运维/CLI 报告中心状态。 type CentroidStatus struct { Saved bool `json:"saved"`