4.5 KiB
Agent Note: 本地 spill 文件的一次性启动清理
Status: implemented
English | 中文
问题
本地 spill 后端从不删除它写下的完整工具结果。每个超限结果都会新增一个文件,因此配置的根目录会无限增长,而每进程默认的 dsh-spill-* 根目录也会跨多次运行不断累积。立即删除是错误的,因为已持久化、已恢复和已 fork 的会话仍可能引用某个 locator。工具输出 spill 策略需要一个有界的本地存储生命周期。
决策
dsh-spill-local 在激活后运行一次尽力而为的清理扫描。它不延迟服务可用性,由插件 fiber 拥有(一个 ctx.effect,其生成器启动该扫描并让出一个等待它的异步 disposer),并在 dispose 期间被等待,因此没有扫描 I/O 会存活到 fiber 之后。既没有周期性定时器,也没有独立进程。
cleanupPeriodDays 配置默认为 30;0 会禁用清理。Schemastery 会在加载时拒绝负数或小数。扫描会遍历配置的/活动的根目录,以及在 OS 临时目录下发现的任何先前默认 dsh-spill-* 临时根目录,并删除 mtime 严格早于 now − cleanupPeriodDays 的常规文件。它会修剪所有空会话目录,但只删除发现的先前默认根目录本身;如果修剪与写入发生竞争,写入操作会重新创建会话目录。根目录别名按设备/inode 身份去重,配置目录的身份会覆盖发现的匹配项,并标记为活动且不可删除。扫描使用 lstat,因此符号链接绝不会被跟随或删除;无关条目(非 session- 目录、特殊文件)会被跳过。每一次文件系统失败都会被捕获并通过 ctx.logger.warn 记录,警告接收方抛出的异常也会被兜底——扫描绝不抛出,因此它无法让激活失败,也无法影响并发的 spill 写入。
基于路径的删除仅限于不受信任的本地 OS 用户无法在扫描期间替换的目录。在 POSIX 上,每个根目录和会话目录都必须由当前用户拥有,且组用户和其他用户不可写;根目录的祖先路径也必须不可写,或由 /tmp 这类 sticky 目录保护。发现过程拒绝符号链接,而配置的符号链接可以解析到可信目标并参与身份去重。不安全路径会被跳过并记录警告。与后端的私有本地存储模型一致,同一用户账号仍是信任边界。
无 ctx 依赖的扫描机制位于 packages/spill/spill-local/src/cleanup.ts(sweepSpillRoots、discoverDefaultRoots),无需 ctx 即可做单元测试;store.ts 负责根目录命名、路径推导与写入,而 src/index.ts 中的服务负责配置、截止时间以及 fiber 拥有的启动/等待。
考虑过的替代方案
运行周期性定时器。 已否决,因为它引入了定时器生命周期、重叠控制以及又一个间隔旋钮。长期运行的进程可能会保留文件直到重启。
在会话 dispose 时删除 spill。 已否决,因为持久会话、恢复和 fork 都会保留 locator。
递归删除旧的会话目录。 已否决,因为并发进程可能在年龄检查之后创建一个新的 spill。按文件过期可保留新写入。
将清理绑定到会话持久化删除。 已否决,因为持久化 seam 没有共同的删除生命周期,而本地后端还独立拥有临时根目录。
后果
清理让后端付出了一次启动扫描和一个配置旋钮的代价,换来了无需定时器、守护进程或会话生命周期耦合的有界本地存储生命周期。并发进程可能重复启动 I/O;严格的过滤与幂等的文件删除保证了这一点的安全。长期运行的进程在重启前不会再次被清理,而这种保留是刻意的——旧的模型可见 locator 只有在超过截止时间后才会失效。seam 本身仍不定义任何保留策略——这是本地后端的关切。
验证
dsh-spill-local 单元测试覆盖了精确年龄边界、cleanupPeriodDays: 0 的禁用、空会话目录与发现根目录的修剪、符号链接/无关条目的跳过、配置根加发现根的覆盖、经配置符号链接验证的文件系统身份去重、不安全 POSIX 根目录/会话目录拒绝、加载期配置校验、文件系统与警告接收方故障兜底,以及静止契约。另一个测试会通过真实 Loader 和 cordis.yml 启动插件,并在 dispose 后观察按配置执行的过期与目录修剪。