mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-01 15:02:53 +00:00
refactor(memory): 移除 media_refs/引用计数,媒体成为一等记忆块
媒体此前是"文本块 + 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 模型目录接线改动。
This commit is contained in:
@ -6,6 +6,8 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
doc "gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||||
@ -81,37 +83,87 @@ func sdkDigestsIn(s string) map[string]bool {
|
||||
return out
|
||||
}
|
||||
|
||||
// sdkBindText 把文本里引用的媒体挂到 owner 上,返回新挂上的条数。
|
||||
//
|
||||
// 短 digest 补全失败(内容已 GC、或前缀有歧义)就跳过那一条:挂一条对不上的
|
||||
// 引用比不挂更糟——owner_kind/owner_id/digest 三者进了主键,digest 错了则
|
||||
// DropOwner 永远匹配不到它,那是一条永久泄漏的引用。
|
||||
//
|
||||
// done 记录本次已处理过的 digest。AddRef 幂等,重复挂不会多出一条引用,
|
||||
// 但会让计数虚高——文档路径先按附件挂一遍、再扫正文标记挂一遍,
|
||||
// 同一份媒体会被数两次,日志里「绑定 2 个」而实际只有 1 条引用。
|
||||
func sdkBindText(ms *media.Store, text, ownerKind, ownerID string, done map[string]bool) int {
|
||||
if ms == nil || text == "" || ownerID == "" {
|
||||
return 0
|
||||
// sdkBlockSeq 保证块 ID 全局唯一:Graph 的 memory_blocks 以 id 为主键,
|
||||
// 不同文档里序号相同的块会在 L2→L3 迁移时相互覆盖。
|
||||
var sdkBlockSeq int64
|
||||
|
||||
func sdkNewBlockID() string {
|
||||
return fmt.Sprintf("blk_%d_%d", time.Now().UnixNano(), atomic.AddInt64(&sdkBlockSeq, 1))
|
||||
}
|
||||
|
||||
// sdkBlockModality 把 CAS 的媒体大类映射为一等记忆块的模态。
|
||||
func sdkBlockModality(k media.Kind) memory.BlockModality {
|
||||
switch k {
|
||||
case media.KindImage:
|
||||
return memory.BlockImage
|
||||
case media.KindVideo:
|
||||
return memory.BlockVideo
|
||||
case media.KindAudio:
|
||||
return memory.BlockAudio
|
||||
default:
|
||||
return memory.BlockText
|
||||
}
|
||||
bound := 0
|
||||
}
|
||||
|
||||
// sdkBlockForDigest 把一份已入库的媒体变成一个一等记忆块。
|
||||
// 块自身携带 digest/向量/ fingerprint;CAS 只提供字节与元数据,不参与生命周期。
|
||||
func sdkBlockForDigest(ms *media.Store, digest string) (memory.MemoryBlock, bool) {
|
||||
it, err := ms.Stat(digest)
|
||||
if err != nil || it == nil {
|
||||
return memory.MemoryBlock{}, false
|
||||
}
|
||||
return memory.MemoryBlock{
|
||||
ID: sdkNewBlockID(),
|
||||
Modality: sdkBlockModality(it.Kind),
|
||||
PayloadDigest: it.Digest,
|
||||
MIME: it.MIME,
|
||||
Size: it.Size,
|
||||
Width: it.Width,
|
||||
Height: it.Height,
|
||||
Vector: it.Vec,
|
||||
Fingerprint: it.VecModel,
|
||||
Tool: it.Tool,
|
||||
CreatedAt: it.FirstSeen,
|
||||
}, true
|
||||
}
|
||||
|
||||
// sdkBlocksFromText 把文本标记里的媒体变成一等块(去重)。
|
||||
func sdkBlocksFromText(ms *media.Store, text string) []memory.MemoryBlock {
|
||||
if ms == nil || text == "" {
|
||||
return nil
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
var blocks []memory.MemoryBlock
|
||||
for _, m := range sdkMarkerPattern.FindAllStringSubmatch(text, -1) {
|
||||
full, err := ms.ResolvePrefix(m[2])
|
||||
if err != nil {
|
||||
if err != nil || seen[full] {
|
||||
continue
|
||||
}
|
||||
if done != nil && done[full] {
|
||||
continue
|
||||
seen[full] = true
|
||||
if b, ok := sdkBlockForDigest(ms, full); ok {
|
||||
blocks = append(blocks, b)
|
||||
}
|
||||
if err := ms.AddRef(full, ownerKind, ownerID); err != nil {
|
||||
continue
|
||||
}
|
||||
if done != nil {
|
||||
done[full] = true
|
||||
}
|
||||
bound++
|
||||
}
|
||||
return bound
|
||||
return blocks
|
||||
}
|
||||
|
||||
// sdkBlocksFromDigests 为显式 digest 列表构造一等块(去重)。
|
||||
func sdkBlocksFromDigests(ms *media.Store, digests []string) []memory.MemoryBlock {
|
||||
if ms == nil || len(digests) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
var blocks []memory.MemoryBlock
|
||||
for _, d := range digests {
|
||||
if d == "" || seen[d] {
|
||||
continue
|
||||
}
|
||||
seen[d] = true
|
||||
if b, ok := sdkBlockForDigest(ms, d); ok {
|
||||
blocks = append(blocks, b)
|
||||
}
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
// sdkPutAttachment 把一份附件解析成完整 digest。
|
||||
@ -208,11 +260,10 @@ func (m *graphMemory) Recall(query []string, depth int) ([]Entity, []Relation, e
|
||||
return entities, relations, nil
|
||||
}
|
||||
|
||||
// Commit 把插件的三元组写入图库,并把三元组引用的媒体挂到句子上。
|
||||
// Commit 把插件的三元组写入图库,并把三元组句子里的媒体变成 L3 一等块。
|
||||
//
|
||||
// 媒体的绑定链是 SentenceText → sentences 表 → sentence_id → media_refs。
|
||||
// 旧实现丢掉 SentenceText 又走 Commit(不回 sentenceIDs),这条链一步都走不通:
|
||||
// 插件即便按格式写好标记,媒体也永远挂不上。
|
||||
// 媒体的落点链是 SentenceText → sentences 表 → sentence_id → 块边。
|
||||
// 旧实现丢掉 SentenceText 又走 Commit(不回 sentenceIDs),这条链一步都走不通。
|
||||
func (m *graphMemory) Commit(triples []Triple) error {
|
||||
if m.db == nil {
|
||||
return nil
|
||||
@ -280,9 +331,11 @@ func (m *graphMemory) sentenceWithMedia(sentence string, digests []string) strin
|
||||
return sentence + "\n" + strings.Join(add, "\n")
|
||||
}
|
||||
|
||||
// bindSentences 把每条句子里引用的媒体挂到该句子的 graph_sentence owner 上。
|
||||
// bindSentences 把每条句子里引用的媒体变成 L3 的一等记忆块,
|
||||
// 并建立 sentence --contains--> block 的结构边。
|
||||
// 不再写 media_refs:块本身就是图的一部分,不需要 owner 账本保活。
|
||||
func (m *graphMemory) bindSentences(sentenceIDs map[string]int64) {
|
||||
if m.ms == nil || len(sentenceIDs) == 0 {
|
||||
if m.ms == nil || m.db == nil || len(sentenceIDs) == 0 {
|
||||
return
|
||||
}
|
||||
bound := 0
|
||||
@ -290,13 +343,20 @@ func (m *graphMemory) bindSentences(sentenceIDs map[string]int64) {
|
||||
if sid == 0 {
|
||||
continue
|
||||
}
|
||||
// 每条句子一个独立的 done 集:同一份媒体挂在不同句子上是两条
|
||||
// 合法引用(owner_id 不同),不该被跨句子去重。
|
||||
bound += sdkBindText(m.ms, text, media.OwnerGraphSentence,
|
||||
strconv.FormatInt(sid, 10), map[string]bool{})
|
||||
for _, b := range sdkBlocksFromText(m.ms, text) {
|
||||
if err := m.db.PutMemoryBlocks([]memory.MemoryBlock{b}); err != nil {
|
||||
log.Printf("[sdk media] 插件 %s 写入 L3 记忆块失败: %v", m.plugin, err)
|
||||
continue
|
||||
}
|
||||
if err := m.db.AddMemoryBlockEdge("sentence", strconv.FormatInt(sid, 10), "block", b.ID, "contains"); err != nil {
|
||||
log.Printf("[sdk media] 插件 %s 建立句子→块边失败: %v", m.plugin, err)
|
||||
continue
|
||||
}
|
||||
bound++
|
||||
}
|
||||
}
|
||||
if bound > 0 {
|
||||
log.Printf("[sdk media] 插件 %s 的三元组绑定 %d 个媒体引用", m.plugin, bound)
|
||||
log.Printf("[sdk media] 插件 %s 的三元组写入 %d 个 L3 记忆块", m.plugin, bound)
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,7 +405,7 @@ func NewTextMemoryWithMedia(plugin string, tm *text.Memory, ms *media.Store) Tex
|
||||
|
||||
// Append 追加一条文本事件;带附件时把媒体标记并进正文。
|
||||
//
|
||||
// 文本记忆是追加写 JSONL,没有稳定 owner_id 可挂 media_refs,所以媒体在这一层
|
||||
// 文本记忆是追加写 JSONL,没有结构化块存储,所以媒体在这一层
|
||||
// 只能以标记形式存在。这不是妥协——描述文本才是持久的语义记忆,blob 只是缓存。
|
||||
func (m *textMemoryImpl) Append(evt TextEvent) error {
|
||||
if m.tm == nil {
|
||||
@ -431,40 +491,56 @@ func (m *docMemoryImpl) Query(text string, topK int) []*Doc {
|
||||
out := make([]*Doc, len(got))
|
||||
for i, d := range got {
|
||||
out[i] = &Doc{ID: d.ID, Title: d.Summary, Content: d.Content}
|
||||
m.fillMedia(out[i])
|
||||
m.fillMedia(out[i], d)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// fillMedia 填充文档的媒体字段。
|
||||
//
|
||||
// 优先用 media_refs(权威:谁挂上去的就是谁),为空时退回解析正文标记——
|
||||
// 历史文档与经旧版插件写入的文档只有标记、没有引用。
|
||||
func (m *docMemoryImpl) fillMedia(out *Doc) {
|
||||
// 优先读一等记忆块(文档直接持有),为空时退回解析正文标记——
|
||||
// 历史文档与经旧版插件写入的文档只有标记、没有块。
|
||||
func (m *docMemoryImpl) fillMedia(out *Doc, d *doc.Doc) {
|
||||
if m.ms == nil {
|
||||
return
|
||||
}
|
||||
digests, err := m.ms.Refs(media.OwnerDocument, out.ID)
|
||||
if err != nil {
|
||||
log.Printf("[sdk media] 读取文档 %s 的媒体引用失败: %v", out.ID, err)
|
||||
}
|
||||
if len(digests) == 0 {
|
||||
out.Attachments = sdkAttachmentsFromText(m.ms, out.Content)
|
||||
for _, a := range out.Attachments {
|
||||
out.MediaDigests = append(out.MediaDigests, a.Digest)
|
||||
if d != nil && len(d.Blocks) > 0 {
|
||||
for _, b := range d.Blocks {
|
||||
if b.PayloadDigest == "" {
|
||||
continue
|
||||
}
|
||||
out.MediaDigests = append(out.MediaDigests, b.PayloadDigest)
|
||||
att := MediaAttachment{Digest: b.PayloadDigest, MIME: b.MIME, Description: ""}
|
||||
if it, err := m.ms.Stat(b.PayloadDigest); err == nil && it != nil {
|
||||
att.MIME = it.MIME
|
||||
att.Description = it.Description
|
||||
}
|
||||
out.Attachments = append(out.Attachments, att)
|
||||
}
|
||||
return
|
||||
}
|
||||
out.MediaDigests = digests
|
||||
for _, d := range digests {
|
||||
it, err := m.ms.Stat(d)
|
||||
if err != nil || it == nil {
|
||||
out.Attachments = sdkAttachmentsFromText(m.ms, out.Content)
|
||||
for _, a := range out.Attachments {
|
||||
out.MediaDigests = append(out.MediaDigests, a.Digest)
|
||||
}
|
||||
}
|
||||
|
||||
// appendBlocks 把新的块追加到已有块之后(按 digest 去重)。
|
||||
func appendBlocks(existing []memory.MemoryBlock, add []memory.MemoryBlock) []memory.MemoryBlock {
|
||||
seen := make(map[string]bool, len(existing))
|
||||
for _, b := range existing {
|
||||
seen[b.PayloadDigest] = true
|
||||
}
|
||||
for _, b := range add {
|
||||
if b.PayloadDigest != "" && seen[b.PayloadDigest] {
|
||||
continue
|
||||
}
|
||||
out.Attachments = append(out.Attachments, MediaAttachment{
|
||||
Digest: it.Digest, MIME: it.MIME, Description: it.Description,
|
||||
})
|
||||
existing = append(existing, b)
|
||||
if b.PayloadDigest != "" {
|
||||
seen[b.PayloadDigest] = true
|
||||
}
|
||||
}
|
||||
return existing
|
||||
}
|
||||
|
||||
// Insert 写入文档。正文里已有的媒体标记会被挂成文档级引用,
|
||||
@ -487,14 +563,17 @@ func (m *docMemoryImpl) InsertWithMedia(d *Doc, attachments []MediaAttachment) e
|
||||
|
||||
digests := m.storeAttachments(attachments, &target.Content)
|
||||
|
||||
// 一等记忆块:文档直接持有块本身,CAS 只提供字节与向量。
|
||||
// 不再写 media_refs——块随文档一同存活或被删除,无需 owner 账本。
|
||||
target.Blocks = appendBlocks(target.Blocks,
|
||||
append(sdkBlocksFromDigests(m.ms, digests), sdkBlocksFromText(m.ms, target.Content)...))
|
||||
|
||||
if err := m.ds.Insert(target); err != nil {
|
||||
return err
|
||||
}
|
||||
// 回填给调用方:ID 是新建时内核生成的,Content 含内核补的标记。
|
||||
d.ID = target.ID
|
||||
d.Content = target.Content
|
||||
|
||||
m.bindDocMedia(target, digests)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -531,51 +610,39 @@ func (m *docMemoryImpl) storeAttachments(atts []MediaAttachment, content *string
|
||||
return digests
|
||||
}
|
||||
|
||||
// bindDocMedia 把附件与正文标记引用的媒体一起挂到文档 owner 上。
|
||||
func (m *docMemoryImpl) bindDocMedia(target *doc.Doc, digests []string) {
|
||||
if m.ms == nil || target.ID == "" {
|
||||
return
|
||||
}
|
||||
bound := 0
|
||||
done := make(map[string]bool, len(digests))
|
||||
for _, full := range digests {
|
||||
if done[full] {
|
||||
continue
|
||||
}
|
||||
if err := m.ms.AddRef(full, media.OwnerDocument, target.ID); err != nil {
|
||||
log.Printf("[sdk media] 文档引用绑定失败 (%s → doc %s): %v",
|
||||
sdkShortDigest(full), target.ID, err)
|
||||
continue
|
||||
}
|
||||
done[full] = true
|
||||
bound++
|
||||
}
|
||||
// 插件手写在正文里的标记同样要挂上,否则那些媒体在文档里可见却无主。
|
||||
// 共用 done:附件刚挂过的那些是同一份媒体(内核自己把标记补进了正文)。
|
||||
bound += sdkBindText(m.ms, target.Content, media.OwnerDocument, target.ID, done)
|
||||
if bound > 0 {
|
||||
log.Printf("[sdk media] 插件 %s 写入文档 %s,绑定 %d 个媒体引用",
|
||||
m.plugin, target.ID, bound)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove 删除文档,同时释放它持有的媒体引用。
|
||||
// Remove 删除文档,并删除它持有的一等块所对应的内容(无其他块共享时)。
|
||||
//
|
||||
// 旧实现只删文档不解引用,于是那些媒体永久处于「被引用」状态:GC 不回收,
|
||||
// 磁盘只增不减。内核的归档路径(distill 的 releaseDocMedia)做了这一步,
|
||||
// 插件路径漏了同一步。
|
||||
// 与文本块一致:删除块即删除内容。媒体字节是块的内容存储,
|
||||
// 不单独做引用计数或 GC。
|
||||
func (m *docMemoryImpl) Remove(id string) {
|
||||
if m.ds == nil {
|
||||
return
|
||||
}
|
||||
if m.ms != nil && id != "" {
|
||||
if n, err := m.ms.DropOwner(media.OwnerDocument, id); err != nil {
|
||||
log.Printf("[sdk media] 释放文档 %s 的媒体引用失败: %v", id, err)
|
||||
} else if n > 0 {
|
||||
log.Printf("[sdk media] 文档 %s 删除,释放 %d 个媒体引用", id, n)
|
||||
var digests []string
|
||||
if d := m.ds.Get(id); d != nil {
|
||||
for _, b := range d.Blocks {
|
||||
if b.PayloadDigest != "" {
|
||||
digests = append(digests, b.PayloadDigest)
|
||||
}
|
||||
}
|
||||
}
|
||||
m.ds.Remove(id)
|
||||
if m.ms == nil {
|
||||
return
|
||||
}
|
||||
// 仍被其它文档持有的 digest 不能删(同一份字节可能被多个块共享)。
|
||||
stillHeld := map[string]bool{}
|
||||
for _, b := range m.ds.Blocks() {
|
||||
stillHeld[b.PayloadDigest] = true
|
||||
}
|
||||
for _, d := range digests {
|
||||
if stillHeld[d] {
|
||||
continue
|
||||
}
|
||||
if err := m.ms.Delete(d); err != nil {
|
||||
log.Printf("[sdk media] 删除文档 %s 的内容失败 %s: %v", id, sdkShortDigest(d), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *docMemoryImpl) Stats() map[string]interface{} {
|
||||
|
||||
@ -30,7 +30,7 @@ func newTestStores(t *testing.T) (*memory.GraphDB, *doc.Store, *text.Memory, *me
|
||||
}
|
||||
t.Cleanup(func() { g.Close() })
|
||||
|
||||
ds := doc.NewStore(filepath.Join(dir, "documents"))
|
||||
ds := doc.NewStore(filepath.Join(dir, "documents"), memory.TokenizeWords)
|
||||
if err := ds.Start(); err != nil {
|
||||
t.Fatalf("doc store start: %v", err)
|
||||
}
|
||||
@ -42,7 +42,7 @@ func newTestStores(t *testing.T) (*memory.GraphDB, *doc.Store, *text.Memory, *me
|
||||
}
|
||||
t.Cleanup(func() { tm.Stop() })
|
||||
|
||||
ms, err := media.New(filepath.Join(dir, "media"), 0)
|
||||
ms, err := media.New(filepath.Join(dir, "media"))
|
||||
if err != nil {
|
||||
t.Fatalf("media.New: %v", err)
|
||||
}
|
||||
@ -140,12 +140,12 @@ func TestGraphCommit_BindsMediaFromDigests(t *testing.T) {
|
||||
t.Errorf("句子里没有媒体描述: %q", res.Relations[0].SentenceText)
|
||||
}
|
||||
|
||||
refs, err := ms.Refs(media.OwnerGraphSentence, strconv.FormatInt(sid, 10))
|
||||
blocks, err := g.BlocksForNode("sentence", strconv.FormatInt(sid, 10))
|
||||
if err != nil {
|
||||
t.Fatalf("Refs: %v", err)
|
||||
t.Fatalf("BlocksForNode: %v", err)
|
||||
}
|
||||
if len(refs) != 1 || refs[0] != digest {
|
||||
t.Errorf("句子 #%d 的媒体引用 = %v,期望 [%s]", sid, refs, digest)
|
||||
if len(blocks) != 1 || blocks[0].PayloadDigest != digest {
|
||||
t.Errorf("句子 #%d 的媒体块 = %+v,期望 [%s]", sid, blocks, digest)
|
||||
}
|
||||
}
|
||||
|
||||
@ -250,15 +250,12 @@ func TestDocInsertWithMedia_StoresAndBinds(t *testing.T) {
|
||||
t.Errorf("正文里没有媒体标记: %q", d.Content)
|
||||
}
|
||||
|
||||
refs, err := ms.Refs(media.OwnerDocument, d.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Refs: %v", err)
|
||||
}
|
||||
if len(refs) != 1 {
|
||||
t.Fatalf("文档媒体引用 = %v,期望 1 条", refs)
|
||||
blocks := ds.Blocks()
|
||||
if len(blocks) != 1 || blocks[0].PayloadDigest == "" {
|
||||
t.Fatalf("文档记忆块 = %+v,期望 1 条", blocks)
|
||||
}
|
||||
// 内容可读,说明真的落盘了而不只是记了个 digest。
|
||||
got, err := ms.Get(refs[0])
|
||||
got, err := ms.Get(blocks[0].PayloadDigest)
|
||||
if err != nil || string(got) != "attachment-bytes" {
|
||||
t.Errorf("媒体内容读回失败: %v / %q", err, got)
|
||||
}
|
||||
@ -279,9 +276,9 @@ func TestDocInsertWithMedia_DigestOnlyReference(t *testing.T) {
|
||||
if after := ms.Stats()["count"]; after != before {
|
||||
t.Errorf("媒体条数从 %v 变成 %v —— 引用已有内容不该新增", before, after)
|
||||
}
|
||||
refs, _ := ms.Refs(media.OwnerDocument, d.ID)
|
||||
if len(refs) != 1 || refs[0] != digest {
|
||||
t.Errorf("引用 = %v,期望 [%s]", refs, digest)
|
||||
blocks := ds.Blocks()
|
||||
if len(blocks) != 1 || blocks[0].PayloadDigest != digest {
|
||||
t.Errorf("引用 = %+v,期望 [%s]", blocks, digest)
|
||||
}
|
||||
}
|
||||
|
||||
@ -350,8 +347,8 @@ func TestDocQuery_FallsBackToMarkers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 旧实现删文档不解引用 → 媒体永久"被引用",GC 收不掉,磁盘只增不减。
|
||||
func TestDocRemove_ReleasesMediaRefs(t *testing.T) {
|
||||
// 文档被删除时它持有的一等记忆块随之消失,媒体不再被任何记忆块持有。
|
||||
func TestDocRemove_DropsBlocks(t *testing.T) {
|
||||
_, ds, _, ms := newTestStores(t)
|
||||
dm := NewDocMemoryWithMedia("tester", ds, ms)
|
||||
|
||||
@ -361,14 +358,14 @@ func TestDocRemove_ReleasesMediaRefs(t *testing.T) {
|
||||
}}); err != nil {
|
||||
t.Fatalf("InsertWithMedia: %v", err)
|
||||
}
|
||||
if refs, _ := ms.Refs(media.OwnerDocument, d.ID); len(refs) != 1 {
|
||||
t.Fatalf("前置条件不成立,引用 = %v", refs)
|
||||
if blocks := ds.Blocks(); len(blocks) != 1 {
|
||||
t.Fatalf("前置条件不成立,块 = %+v", blocks)
|
||||
}
|
||||
|
||||
dm.Remove(d.ID)
|
||||
|
||||
if refs, _ := ms.Refs(media.OwnerDocument, d.ID); len(refs) != 0 {
|
||||
t.Errorf("删除文档后仍有 %v 条引用 —— GC 永远收不掉这份媒体", refs)
|
||||
if blocks := ds.Blocks(); len(blocks) != 0 {
|
||||
t.Errorf("删除文档后仍持有 %+v —— 媒体仍被记忆引用", blocks)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user