缓存安装:下载完成不等于资产可用

自动关联目录:缓存安装:下载完成不等于资产可用

核心判断:缓存安装不是“把文件写到固定路径”,而是一次跨文件系统与元数据存储的提交协议。文件存在、数据库为 Ready 都只能说明候选制品可发现;只有绑定了制品身份和本次验证结果的凭证,才能授权运行时激活。

一、业务冲突:一份正确的新下载,为什么仍可能加载失败

动态内容的安装链路同时面对下载重试、多进程竞争、启动恢复、空间回收和多版本共存。业务真正需要的是:某个逻辑资产的某个版本,在指定运行目标下已经完整验证,之后读取到的仍是同一份字节。

实际实现中,下载缓冲区会先做完整校验;磁盘已有文件则通过持久化记录、规范文件名和文件大小快速判断“已经安装”。这两个分支单独看都合理,组合后却允许一个具体失败序列:

  1. 旧文件曾成功安装,持久层记录为 Ready;
  2. 文件后来发生等长损坏,路径和大小均未改变;
  3. 新请求下载到一份正确字节,并完成全量校验;
  4. 安装锁内的快速检查把旧文件判为已安装,于是丢弃新缓冲区;
  5. 冷启动校验重新读取旧文件并发现损坏,激活失败;
  6. 下次重试仍可能重复上述过程,因为旧记录没有被隔离或降级。

这不是“再补一次哈希”就能彻底解决的问题。根因是 Ready 同时承担了“发现过文件”“曾经验证过”“现在允许激活”三种不同保证。

二、实际实现:已经形成提交协议,但状态命名掩盖了保证等级

实践中的安装路径已经比普通文件缓存严格得多。下载缓冲区先验证容器结构、分块摘要、尾部元数据、逻辑资产身份、内容摘要、预期大小和可选的传输摘要;通过后才竞争同一资产版本的安装锁。真正写盘时使用同目录唯一临时文件,完成写入和刷新、关闭句柄后,再以替换方式重命名到最终路径。元数据提交失败会删除刚发布的最终文件。

运行时还区分两条激活路径:只有“本任务验证并写入”的分支才能产生进程内验证凭证;复用其他进程的安装结果或重启后恢复的文件,都必须重新读盘做完整校验。校验期间持有租约,避免空间回收删除正在验证的文件。

flowchart LR
    A[下载缓冲区] --> B[全量校验]
    B --> C[资产版本安装锁]
    C --> D[唯一临时文件<br/>写入 + 刷新 + 关闭]
    D --> E[同卷原子重命名]
    E --> F[提交持久化记录]
    F --> G[签发本次安装凭证]
    G --> H[热激活]

    I[其他进程安装 / 启动扫描] --> J[候选文件]
    J --> K[租约保护下重新读盘]
    K --> L[全量校验 + 身份复核]
    L --> M[签发冷验证凭证]
    M --> H

启动阶段会清理遗留临时文件、失效锁和失效租约;数据库损坏时可以重建,再从磁盘扫描候选文件;空间回收则跳过正在安装、验证或使用的版本。这些机制已经构成了提交、恢复和并发保护的基本骨架。

三、值得保留:验证凭证应当是一种能力,而不是布尔值

当前设计最有价值的部分,不是临时文件,而是“不让来源不明的成功共享热激活资格”。

  • 发布前验证。 下载完成不直接写最终路径,损坏数据没有机会伪装成可用制品。
  • 唯一临时文件与同卷替换。 并发任务不会互相截断临时文件;读者只看到旧版本或完整新版本,不看到半写文件。
  • 安装锁只保护提交区。 网络传输和大部分校验不长期占锁;迟到任务也不会轻易覆盖已提交结果。
  • 验证凭证绑定完整身份。 凭证包含逻辑资产、内容版本、解析代次、最终路径和已解码元数据;其中任一项变化,旧凭证都不能复用。
  • 其他进程不能继承热凭证。 共享磁盘结果可以复用字节,不能复用另一个进程内不可验证的事实。
  • 校验租约与使用租约分离。 空间回收既不能删除正在读取的文件,也不能删除已经被运行时持有的版本;多个版本可以独立存在和淘汰。

这里可以抽象出一条通用原则:验证结果不是文件的永久属性,而是某个验证者对“身份 + 字节快照”的能力证明。 文件路径、大小、时间戳只能帮助快速拒绝,不能单独授予激活权限。

四、结构性缺陷:持久层把发现、验证和提交压成了一个 Ready

目前同一个 Ready 可能来自四种强度不同的事实:本进程完成全量校验并安装;其他进程留下了匹配大小的文件;启动扫描读到了可解析的尾部元数据;旧记录仍在但对应文件已经缺失。调用方看到相同状态,却无法知道它究竟获得了哪一级保证。

这会产生三个结构性问题。

第一,快速复用检查只比较路径、规范文件名和大小时,能够阻止并发重复写,却不能区分“正确旧文件”和“等长损坏旧文件”。它本应只返回 Present,却被当成了 Installed。

第二,文件重命名与数据库提交属于两个不同存储系统,不存在真正的联合事务。当前通过“元数据提交失败就删文件”和“启动时扫描收编”缩小窗口,但启动收编只解析结构和尾部信息便标记 Ready,仍把“可发现”写成了“已验证”。同样,空间回收先删文件、再删记录,崩溃后也会留下陈旧状态。

第三,写入刷新加原子重命名保证的是读者可见性的原子性,不自动等于断电后的持久性。是否需要目录级持久化屏障,应由平台能力和内容可重下载性决定,不能从一次成功重命名推导出来。

因此真正需要重构的不是某一个错误分支,而是持久状态表达的保证等级,以及状态不一致后的收敛规则。

五、目标设计:Present、Verified、Committed 必须分层

目标方案把“磁盘发现”与“运行授权”拆开。持久层至少表达 PresentUnverified、Verified、Committed 和 Quarantined:启动扫描最多进入 PresentUnverified;全量校验后才能进入 Verified;元数据事务完成、且文件身份仍与验证快照一致后,才进入 Committed。运行时只接受与当前请求匹配的 ActivationTicket,不直接相信状态字符串。

Install(candidate, key):
    proof = validateAll(candidate.bytes, key)

    with installLock(key):
        existing = catalog.lookup(key)
        if existing is not null:
            oldProof = validateExistingOrUseProcessLocalProof(existing, key)
            if oldProof.digest == proof.digest:
                return issueTicket(oldProof)
            quarantine(existing)

        intent = recoveryJournal.stage(key, uniqueTempPath(), proof.digest)
        writeFlushAndClose(intent.tempPath, candidate.bytes)
        atomicRenameSameVolume(intent.tempPath, immutablePath(key, proof.digest))

        catalog.register(key, proof.digest, PresentUnverified)
        diskProof = recheckFileIdentity(proof, immutablePath(key, proof.digest))

        catalog.transaction:
            catalog.promote(key, Verified)
            catalog.publishActiveRevision(key, proof.digest)
            catalog.promote(key, Committed)
            recoveryJournal.clear(intent)

        return issueTicket(diskProof)

Recover():
    cleanDeadLocksLeasesAndTemps()
    for each journalIntent or discoveredFinalFile:
        classifyAs(PresentUnverified)
        verifyThenPromoteOrQuarantine()

这里的关键不是增加状态数量,而是约束每个状态允许做什么:

  • PresentUnverified 可以参与后台校验,不能激活,也不能阻止一份已验证新制品修复它;
  • Verified 表示内容身份已证明,但激活前仍要确认文件身份没有在验证后变化;
  • Committed 表示目录索引已经指向该不可变制品,不代表文件可以被无租约删除;
  • Quarantined 必须退出快速复用路径,并保留足够诊断信息以解释为什么被替换。

文件系统和数据库不能组成一个原子事务,所以恢复日志不是为了模拟完美事务,而是记录“提交进行到哪一步”,让启动恢复可以确定地补做、回滚或重新验证。

六、取舍与验证:根据失败成本选择保证强度

方案能证明什么主要代价适用边界
文件存在 + 大小匹配大概率已经下载过最低,但等长损坏不可见可丢弃、失败后无感重建的小缓存
持久化 Ready + 启动扫描可快速发现和恢复候选文件状态容易陈旧,保证等级含混只能作为发现层,不能直接授权激活
不可变路径 + 验证凭证 + 恢复日志当前请求对应的字节已验证,崩溃后可收敛多一次校验、更多状态与故障注入测试动态远端内容、多进程安装、多版本共存

现有测试已经覆盖凭证身份不匹配、冷验证、并发安装、迟到下载、遗留临时文件、失效锁与租约、数据库重建、磁盘收编、版本共存和回收保护。下一阶段最重要的不是继续堆正常路径,而是补四组故障注入:临时文件刷新后崩溃、重命名后但元数据提交前崩溃、元数据提交后但激活前崩溃,以及等长损坏文件与正确新下载同时出现。每个测试都应证明系统最终只收敛到“可验证激活”或“明确隔离”,不能停在伪 Ready。

我的反思是:早期把缓存表设计成下载状态机很自然,因为主要问题是“有没有下载完成”;当系统引入跨进程安装、启动收编、空间回收和热激活后,核心问题已经变成“谁证明了哪份字节可以被谁使用”。如果状态模型没有随业务升级,后续再严密的锁、重试和扫描也只能维护一个语义过载的布尔值。

最终决策规则很简单:发现可以便宜,激活必须有证据;恢复可以延迟,状态不能撒谎。

前置的制品身份见:资产身份:路径能找到文件,但不能证明文件正确。下载阶段的所有权问题见:共享下载:工作可以合并,所有权不能合并。