package main // 本文件把**文档回收(GC)**接进组合根(design/24 §3.2 的配额与清理,切片 S7): // 按配置构造回收器、在启动期清扫一次、此后按间隔定时清扫,退出时干净停下。 // // 三条取舍写在这里(实现细节见 `internal/runtime/documents/gc.go`): // // 1. **启动期清扫不阻塞启动**:它是常驻 goroutine 的第一件事(紧随子系统启动之后 // 开始,网关监听之前就绪),而不是在 newApp/run 里同步等它跑完 —— 数据目录在 // 慢盘/网络盘上时,一次清扫不该把「昔涟能不能起来」绑在磁盘上。 // 2. **失败不退出进程**:查询失败(库不可用、迁移没跑)只记一条 WARN 并等下一轮; // 单件文件删除失败由 gc.go 记 WARN 后继续,整轮照常跑完。 // 3. **关闭 = 显式配 0**:`DOCUMENT_GC_INTERVAL=0` 或 `DOCUMENT_RETENTION=0` // 都不构造回收器,NewGC 会把关闭原因记进日志(静默的「配了却不生效」不存在)。 import ( "context" "log/slog" "path/filepath" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/documents" ) // documentGC 是常驻的文档回收任务:一个回收器 + 一个可停的循环。 type documentGC struct { gc *documents.GC log *slog.Logger cancel context.CancelFunc done chan struct{} } // newDocumentGC 按当前配置构造回收器;GC 关闭时返回 nil(NewGC 已记下关闭原因)。 // 目录口径与网关的上传目录、摄取层的读取目录**逐字同源** // (`/uploads/documents`,sidecar 在 `/extracted`)。 func (a *app) newDocumentGC() *documentGC { docDataDir := resolveDataDir(a.cfg) gc := documents.NewGC(documents.GCConfig{ DB: a.db, DataDir: docDataDir, UploadsDir: filepath.Join(docDataDir, "uploads", "documents"), Interval: a.cfg.Server.DocumentGCInterval, Retention: a.cfg.Server.DocumentRetention, Log: a.log, }) if !gc.Enabled() { return nil } return &documentGC{gc: gc, log: logger.WithSource(a.log, "cmd/cyrene")} } // startDocumentGC 拉起常驻回收(无操作 = GC 关闭):立刻清扫一次,然后按间隔定时 // 清扫,直到 ctx 被取消或 stop 被调用。 func (a *app) startDocumentGC(ctx context.Context) { runner := a.newDocumentGC() if runner == nil { return } runCtx, cancel := context.WithCancel(ctx) runner.cancel = cancel runner.done = make(chan struct{}) a.docGC = runner go runner.loop(runCtx) } // loop 是回收循环:启动期清扫一次 → 定时清扫。 func (d *documentGC) loop(ctx context.Context) { defer close(d.done) d.sweep(ctx) ticker := time.NewTicker(d.gc.Interval()) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: d.sweep(ctx) } } } // sweep 跑一轮回收。每轮的成功汇总由 gc.Sweep 自己记(删了几件/释放多少字节, // **绝不记正文**);这里只负责把「这一轮整个没跑成」记成 WARN 并让循环继续。 func (d *documentGC) sweep(ctx context.Context) { if _, err := d.gc.Sweep(ctx); err != nil { d.log.Warn("document gc sweep failed", "err", err) } } // stop 停掉回收循环并等它退出;未启动时是空操作。它必须在关库**之前**调用 // (清扫要用那条连接池)。 func (d *documentGC) stop() { if d == nil || d.cancel == nil { return } d.cancel() <-d.done }