feat(documents): 引用计数 + 文档/侧车 GC(主人 2.2 裁决;S7)
- 引用计数真实维护(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 不阻塞消息写入 ⇒ 计数可能偏低(只会让文件**晚**回收,绝不早删)。
This commit is contained in:
1 parent
79b1ec36aa
commit
3ba31b58ff
18 files changed
+1951
-29
No files matched your search
@@ -147,8 +147,26 @@ func decodeDocumentData(raw string) ([]DocumentRef, error) {
|
||||
//
|
||||
// 未知 id 返回 ErrMessageNotFound(与 UpdateMessage 同一口径),使调用方能区分
|
||||
// 「那一行不见了」与「写失败」,并据此打带来源的 WARN,而不是静默当作写完了。
|
||||
//
|
||||
// 引用计数按**新旧名字集合的差**在同一事务里增减(切片 S7,见 refs.go):这条
|
||||
// 路径的常态是「同一批名字、只补 status/chars/truncated」,那时加减都是空;只有
|
||||
// 名字集合真的变了才动计数。旧列读不出来(行不存在·读失败)时先于 UPDATE 返回,
|
||||
// 绝不把一次「可能多算」的写留给下一次 GC。
|
||||
func (s *Store) UpdateMessageDocument(ctx context.Context, id, document string) error {
|
||||
res, err := s.db.ExecContext(ctx, `UPDATE chat_messages SET document = ? WHERE id = ?`, document, id)
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("history: update message document %s: %w", id, err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
var previous string
|
||||
err = tx.QueryRowContext(ctx, `SELECT document FROM chat_messages WHERE id = ?`, id).Scan(&previous)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("history: update message document %s: %w", id, ErrMessageNotFound)
|
||||
}
|
||||
return fmt.Errorf("history: read message document %s: %w", id, err)
|
||||
}
|
||||
res, err := tx.ExecContext(ctx, `UPDATE chat_messages SET document = ? WHERE id = ?`, document, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("history: update message document %s: %w", id, err)
|
||||
}
|
||||
@@ -159,6 +177,12 @@ func (s *Store) UpdateMessageDocument(ctx context.Context, id, document string)
|
||||
if n == 0 {
|
||||
return fmt.Errorf("history: update message document %s: %w", id, ErrMessageNotFound)
|
||||
}
|
||||
if err := s.syncDocumentRefs(ctx, tx, previous, document); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("history: update message document %s: %w", id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user