mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 03:52:25 +00:00
test(memory): 媒体存储的压力、冒烟与长稳测试
在 f408ccd 的 CAS 层之上补齐三类验证。
## 压力测试(stress_test.go,9 例)
核心不是吞吐数字,而是并发下的不变量。为此写了 checkRefIntegrity:
用 SQL 对比每个 digest 的 ref_count 与 media_refs 实际行数。这条对不上
就意味着 GC 的判断依据是错的——计数偏低会误删有引用的内容,虚高会让
孤儿永远清不掉。所有并发用例收尾都验它。
- 32 goroutine 并发 Put 同一内容 → digest 一致、磁盘只 1 份
- 400 个不同内容并发入库 → 无丢条目、逐条回读无内容串位
- 24 worker × 40 轮引用增删风暴(含故意重复 AddRef 验并发下的幂等)
- GC 与读写并发 1.5s → 实测 11508 次 Put / 604 轮 GC,受保护内容零失败
- Describe 与 Search/Pending 并发 → 无 database is locked
- 容量上限持续加压 → 上限 256KB 收尾 98KB,有引用项全存活
- 重度 churn 后重开 → 磁盘文件数 == 元数据条数,无双向孤儿
- 4MB 单文件往返(see_video 10 帧 × 2MB 是现实上限附近)
- data URL 往返 ×50(SetToolBlocks 给出的实际形态)
-race -count=3 干净。
## 冒烟测试(smoke_test.go,6 场景)
走真实数据路径:真 PNG(自建 IHDR/IDAT/IEND + zlib)、真 data URL、
真 sha256、真 GC、真重启,而不是随机字节。
- 同一张截图连问 5 轮 → 磁盘 1 份、5 个 context 引用
- see_video 6 帧内容各异 → 各存一份、共享一个 owner
- 描述落库后按关键词检索命中(方案 C 最关键的一环:blob 可被淘汰,
描述会长期留在记忆里)
- L0→L2 归档时引用从 context owner 转到 document owner,期间内容可读
- 别的工具留下的一次性图被 GC 清掉,被记忆引用的一个不少
- 全生命周期跨重启:描述、引用、内容、磁盘一致性全部完好
第一次跑挂在「6 帧只搜到 5 条」,看着像存储丢帧,实际是夹具的
palette[(variant+y*3/h)%5] 只有 5 色,variant=0 与 5 产出逐字节相同的
PNG,被 CAS 正确去重。已把 variant 写进像素保证帧间真不同,并把这段
经过记进注释——误报本身证明了去重在工作,也证明冒烟确实有能力发现
「帧数对不上」这类问题。
冒烟原先是 internal/memory/media/smoke/ 下带 //go:build smoke 的独立
main,得记着加 -tags smoke 才跑得到,那种早晚被忘掉。已搬成普通测试,
随 go test ./... 一起跑,冒烟的意义才真正成立。
## 长稳测试(soak_test.go,-short 下跳过)
60 秒五路混合负载。实测:put=281885 get=1644084 gc=9142
describe=53875 search=23268 refOps=187926,零失败。收尾 20 个受保护项
内容字节一致、ref_count 全为 1;8MB 上限下实际占用 139KB / 48 条,
说明 28 万次写入产生的孤儿被持续清理,无无界增长。
描述者从 Pending() 取项再 Describe(),GC 随时可能在这两步之间清掉它。
这是正常竞态,故忽略 unknown digest 并注明原因;5 万多次调用没把它
升级成计数错位,印证了 Describe 对已删项返回错误而非静默建条目的选择。
全仓 go build / go vet / go test 通过,SDK 接口冻结 diff 为 0。
This commit is contained in:
180
internal/memory/media/soak_test.go
Normal file
180
internal/memory/media/soak_test.go
Normal file
@ -0,0 +1,180 @@
|
||||
package media
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestSoak_SustainedMixedLoad 长稳测试:持续混合负载下不变量不破。
|
||||
// 用 -run TestSoak -timeout 300s 单独跑,默认 short 模式跳过。
|
||||
func TestSoak_SustainedMixedLoad(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("long soak test; run with -run TestSoak")
|
||||
}
|
||||
dur := 60 * time.Second
|
||||
s := newTestStore(t, 8*1024*1024) // 8MB 上限,逼 GC 频繁工作
|
||||
|
||||
// 常驻受保护集
|
||||
const keepN = 20
|
||||
keep := make([]string, keepN)
|
||||
keepData := make([][]byte, keepN)
|
||||
for i := range keep {
|
||||
d := make([]byte, 4096)
|
||||
rand.Read(d)
|
||||
d = append([]byte(fmt.Sprintf("keep-%d-", i)), d...)
|
||||
dg, err := s.Put(d, Item{MIME: "image/png"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.AddRef(dg, "graph_sentence", fmt.Sprintf("s-%d", i)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
keep[i] = dg
|
||||
keepData[i] = d
|
||||
}
|
||||
|
||||
stop := make(chan struct{})
|
||||
var wg sync.WaitGroup
|
||||
var puts, gets, gcs, describes, searches, refOps atomic.Int64
|
||||
var fatal atomic.Int64
|
||||
|
||||
worker := func(name string, fn func(iter int) error) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; ; i++ {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
}
|
||||
if err := fn(i); err != nil {
|
||||
fatal.Add(1)
|
||||
t.Errorf("%s 第 %d 次失败: %v", name, i, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 写入者 ×3
|
||||
for w := 0; w < 3; w++ {
|
||||
wid := w
|
||||
worker(fmt.Sprintf("put-%d", wid), func(i int) error {
|
||||
b := make([]byte, 2048)
|
||||
rand.Read(b)
|
||||
b = append([]byte(fmt.Sprintf("eph-%d-%d-", wid, i)), b...)
|
||||
d, err := s.Put(b, Item{MIME: "image/png", Tool: "cmd_run"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
puts.Add(1)
|
||||
// 三分之一挂上引用再立刻注销,模拟短命引用
|
||||
if i%3 == 0 {
|
||||
own := fmt.Sprintf("tmp-%d-%d", wid, i)
|
||||
if err := s.AddRef(d, "context", own); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.DropRef(d, "context", own); err != nil {
|
||||
return err
|
||||
}
|
||||
refOps.Add(2)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// 读取者 ×3:受保护集必须始终完好
|
||||
for r := 0; r < 3; r++ {
|
||||
worker("get", func(i int) error {
|
||||
idx := i % keepN
|
||||
got, err := s.Get(keep[idx])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(got, keepData[idx]) {
|
||||
return fmt.Errorf("内容被改 %s", shortDigest(keep[idx]))
|
||||
}
|
||||
gets.Add(1)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GC 者
|
||||
worker("gc", func(i int) error {
|
||||
if _, _, err := s.GC(0); err != nil {
|
||||
return err
|
||||
}
|
||||
gcs.Add(1)
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
return nil
|
||||
})
|
||||
|
||||
// 描述者
|
||||
worker("describe", func(i int) error {
|
||||
pend, err := s.Pending(5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, it := range pend {
|
||||
// 忽略 unknown digest:GC 可能在 Pending 与 Describe 之间清掉它,
|
||||
// 这是正常竞态而非缺陷。
|
||||
_ = s.Describe(it.Digest, fmt.Sprintf("描述 %d 含图表与文字", i), "vis")
|
||||
describes.Add(1)
|
||||
}
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
return nil
|
||||
})
|
||||
|
||||
// 检索者
|
||||
worker("search", func(i int) error {
|
||||
if _, err := s.Search("图表", KindImage, 20); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.Stat(keep[i%keepN]); err != nil {
|
||||
return err
|
||||
}
|
||||
searches.Add(1)
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
return nil
|
||||
})
|
||||
|
||||
time.Sleep(dur)
|
||||
close(stop)
|
||||
wg.Wait()
|
||||
|
||||
if n := fatal.Load(); n > 0 {
|
||||
t.Fatalf("%d 个 worker 报致命错误", n)
|
||||
}
|
||||
|
||||
t.Logf("%v 内: put=%d get=%d gc=%d describe=%d search=%d refOps=%d",
|
||||
dur, puts.Load(), gets.Load(), gcs.Load(), describes.Load(), searches.Load(), refOps.Load())
|
||||
|
||||
// 收尾断言
|
||||
for i, d := range keep {
|
||||
got, err := s.Get(d)
|
||||
if err != nil {
|
||||
t.Fatalf("受保护项丢失 %s: %v", shortDigest(d), err)
|
||||
}
|
||||
if !bytes.Equal(got, keepData[i]) {
|
||||
t.Fatalf("受保护项内容变了 %s", shortDigest(d))
|
||||
}
|
||||
it, err := s.Stat(d)
|
||||
if err != nil || it.RefCount != 1 {
|
||||
t.Fatalf("受保护项引用计数应为 1: %+v", it)
|
||||
}
|
||||
}
|
||||
checkRefIntegrity(t, s)
|
||||
|
||||
st := s.Stats()
|
||||
t.Logf("收尾: 条目=%v 字节=%v 未引用=%v 已描述=%v",
|
||||
st["count"], st["total_bytes"], st["unreferenced"], st["described"])
|
||||
if total := st["total_bytes"].(int64); total > 8*1024*1024*3 {
|
||||
t.Fatalf("容量失控: %d 远超上限", total)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user