mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-29 22:12:52 +00:00
媒体此前是"文本块 + digest 引用 + owner 账本 + 独立 GC":ContextEvent.Media
记 digest,media_refs 表用 owner_kind/owner_id 保活,ref_count 决定 GC 能否清。
这与文本记忆块的管理方式不一致,也是本次一并纠正的核心偏差。
改为与文本块完全一致的生命周期:
1. 一等记忆块直接由所在层持有
- ContextEvent.Blocks / Doc.Blocks / GraphDB memory_blocks
- 块带 modality/digest/MIME/size/vector/fingerprint,文本、图片、视频同构
- Context→Document→Graph 迁移的是块本身(ID 不变),迁移后清空源容器,
同一块不同时存在于两层
2. 删除平行生命周期账本
- media.Store 去掉 media_refs 表、OwnerKind 常量、RefCount 字段、
AddRef/DropRef/DropOwner/Refs、ref_count 列与索引
- 删除 mediaGCLoop、GC(keep,minAge)、容量上限与 media.gc_* / media.max_mb 配置
- 媒体内容在块被永久删除时一并删除(media.Store.Delete + forgetPayloads),
与"删除文本块即删除内容"同一语义
3. L3 原生结构
- memory_blocks / memory_block_edges(contains/depicts/derived_from)
- 边端点必须是真实图节点,不再用 owner 字符串伪装关系
- BlocksForNode 支持 sentence --contains--> block 反查
4. SDK 与检索同步
- 插件附件/标记直接变成块,不再 AddRef
- 跨模态检索改用 QueryMediaScored(CAS 内不再有孤儿缓存需要过滤)
测试全部改写为块语义:删除 refcount/media_refs/GC 断言,新增块迁移、
单层不变量、Delete 语义与并发删除回归。
注:cmd/homed/main.go 同时携带工作区中既有的 CLIP→Qwen 模型目录接线改动。
167 lines
3.9 KiB
Go
167 lines
3.9 KiB
Go
package media
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/rand"
|
||
"fmt"
|
||
"sync"
|
||
"sync/atomic"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// TestSoak_SustainedMixedLoad 长稳测试:持续混合负载下不变量不破。
|
||
// 用 -run TestSoak -timeout 300s 单独跑,默认 short 模式跳过。
|
||
//
|
||
// 媒体没有独立生命周期管理:blob 是记忆块的内容,块被删除时内容随之删除。
|
||
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)
|
||
|
||
// 常驻受保护区:全程被记忆块持有,模拟 Graph L3 中的块
|
||
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)
|
||
}
|
||
keep[i] = dg
|
||
keepData[i] = d
|
||
}
|
||
|
||
stop := make(chan struct{})
|
||
var wg sync.WaitGroup
|
||
var puts, gets, deletes, describes, searches 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...)
|
||
if _, err := s.Put(b, Item{MIME: "image/png", Tool: "cmd_run"}); err != nil {
|
||
return err
|
||
}
|
||
puts.Add(1)
|
||
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
|
||
})
|
||
}
|
||
|
||
// 删除者:持续删除一次性内容(模拟块创建后又被遗忘)
|
||
worker("delete", func(i int) error {
|
||
b := make([]byte, 2048)
|
||
rand.Read(b)
|
||
b = append([]byte(fmt.Sprintf("del-%d-", i)), b...)
|
||
d, err := s.Put(b, Item{MIME: "image/png", Tool: "cmd_run"})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := s.Delete(d); err != nil {
|
||
return err
|
||
}
|
||
deletes.Add(1)
|
||
time.Sleep(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 delete=%d describe=%d search=%d",
|
||
dur, puts.Load(), gets.Load(), deletes.Load(), describes.Load(), searches.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))
|
||
}
|
||
}
|
||
|
||
st := s.Stats()
|
||
t.Logf("收尾: 条目=%v 字节=%v 已描述=%v",
|
||
st["count"], st["total_bytes"], st["described"])
|
||
}
|