- 引用计数真实维护(documents 表唯一写入面):增减点只有四处 —— history 的 insert / UpdateMessageDocument / DeleteMessage / DeleteSession,且**全在同一事务**内(计数写失败 ⇒ 消息写入整体回滚,有钉住用例);UpdateMessageDocument 按新旧名字**集合差**增减(三阶段回写是常态,重算会重复计数 ⇒ 文件永远回收不掉)。 - GC:候选 = ref_count = 0;判据 = last_referenced_at(NULL 回落 created_at)早于 now − 保留期;动作 = 删 sidecar + 原件 + 元数据行(删行带 AND ref_count = 0 原子再确认);路径双检;汇总日志(含实占字节);单件失败 WARN 并继续;删除顺序 = 先文件后行(失败则行留着下轮重试,不留孤儿)。 - 组合根:启动期清扫一次(不阻塞启动)+ ticker 常驻(可停,close 顺序在关库之前);env DOCUMENT_GC_INTERVAL 默认 1h / DOCUMENT_RETENTION 默认 168h,**任一 ≤0 = 关闭 GC**(RETENTION=0 不解释成立即回收,关闭时记 WARN);.env.example 已同步(AGENTS §10)。 - 主会话独立验收:隔离 worktree(S7 的 18 文件,排除 F2 在途的 mcp/vision)build/vet 绿、全树 59 包 0 FAIL、GC/引用用例 20 PASS、双平台交叉编译绿;**变异亲自复现**(候选查询去掉 ref_count = 0 ⇒ TestGCDoesNotDeleteReferencedDocument 红:candidates = 1, want 0)。 - 未做(任务书允许的「只做后端」):面板手动「清理未引用附件」入口 —— 后端能力已就绪(documents.NewGC + GC.Sweep),接成端点属小切片(含 admin 路由 + 审计 document.gc + 契约登记)。 - 已知窄窗口(已在 gc.go 文件头登记):一轮 GC 与一次落库几乎同时发生时可能删掉刚被引用的文件(行不删、记 WARN);坏 document 列只记 WARN 不阻塞消息写入 ⇒ 计数可能偏低(只会让文件**晚**回收,绝不早删)。
49 lines
1.7 KiB
Go
49 lines
1.7 KiB
Go
package config
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// TestLoad_DocumentGCKeys 钉住文档回收(GC)两个新键(切片 S7)的三态:
|
||
// 默认(1h / 168h = 7 天)、显式覆盖、以及「0 = 关闭」——后者尤其重要:0 必须被
|
||
// 解码成 0(而不是被当成未配置、悄悄回落到默认开启)。
|
||
func TestLoad_DocumentGCKeys(t *testing.T) {
|
||
t.Setenv("JWT_SECRET", strings.Repeat("s", 40))
|
||
|
||
cfg, err := Load("")
|
||
if err != nil {
|
||
t.Fatalf("Load() error: %v", err)
|
||
}
|
||
if cfg.Server.DocumentGCInterval != time.Hour {
|
||
t.Errorf("DocumentGCInterval = %v, want 1h(默认)", cfg.Server.DocumentGCInterval)
|
||
}
|
||
if cfg.Server.DocumentRetention != 168*time.Hour {
|
||
t.Errorf("DocumentRetention = %v, want 168h(默认 7 天)", cfg.Server.DocumentRetention)
|
||
}
|
||
|
||
t.Setenv("DOCUMENT_GC_INTERVAL", "30m")
|
||
t.Setenv("DOCUMENT_RETENTION", "24h")
|
||
cfg, err = Load("")
|
||
if err != nil {
|
||
t.Fatalf("Load() with overrides: %v", err)
|
||
}
|
||
if cfg.Server.DocumentGCInterval != 30*time.Minute || cfg.Server.DocumentRetention != 24*time.Hour {
|
||
t.Errorf("覆盖后的取值为 (%v,%v), want (30m,24h)",
|
||
cfg.Server.DocumentGCInterval, cfg.Server.DocumentRetention)
|
||
}
|
||
|
||
// 0 = 关闭:必须原样抵达配置(GC 的「关闭」判据是 ≤0)。
|
||
t.Setenv("DOCUMENT_GC_INTERVAL", "0")
|
||
t.Setenv("DOCUMENT_RETENTION", "0")
|
||
cfg, err = Load("")
|
||
if err != nil {
|
||
t.Fatalf("Load() with zeros: %v", err)
|
||
}
|
||
if cfg.Server.DocumentGCInterval != 0 || cfg.Server.DocumentRetention != 0 {
|
||
t.Errorf("显式 0 后的取值为 (%v,%v), want (0,0)(0 = 关闭 GC,不是「回落默认」)",
|
||
cfg.Server.DocumentGCInterval, cfg.Server.DocumentRetention)
|
||
}
|
||
}
|