cache system - mclucy/lucy GitHub Wiki

缓存系统(Cache System)

本文系统化说明 Lucy cache 包的实现与行为,覆盖:

  • 三层架构职责(store / index / policy
  • 缓存条目完整生命周期(初始化、写入、读取、逐出、清空)
  • 完整性校验与并发安全模型
  • 当前已知集成缺口与运行时限制

缓存入口为 cache.Network():该函数返回进程级单例 handler,所有 HTTP 下载通过该 handler 进行 content-addressed 缓存。

  • KindArtifact:JAR、二进制等大文件,长 TTL
  • KindMetadata:版本清单、API 响应等小文件,短 TTL

参见 cache/cache.go:25

// ../lucy/cache/cache.go:1-30
// Package cache provides a content-addressed artifact and metadata cache for
// downloaded files. It is organized in three internal layers:
//
//   - store:  content-addressed blob IO (read/write/ingest/remove)
//   - index:  versioned manifest tracking cache entries with v1→v2 migration
//   - policy: per-kind (artifact vs metadata) TTL and size-limit enforcement
//
// The primary consumer-facing API is [util.CachedDownload], which wraps HTTP
// downloads with streaming hash verification, cache storage, and optional
// progress bar injection via DownloadOptions.WrapReader.
//
// Cache entries are keyed by URL and classified as either KindArtifact (JARs,
// binaries — long TTL) or KindMetadata (version manifests, API responses —
// short TTL). Integrity verification is performed inline during download when
// an expected hash is provided.
package cache

import "sync"

var (
 networkOnce    sync.Once
 networkHandler *handler
)

func Network() *handler {
 networkOnce.Do(func() {
  networkHandler = newHandler("network", DefaultCacheConfig())
 })
 return networkHandler
}

关键要点

架构与数据模型

缓存由三个内部层协同构成:

  1. Store(存储层):管理 content-addressed blob 的磁盘 I/O
  2. Index / Manifest(索引层):管理缓存条目映射并持久化清单
  3. Policy(策略层):按条目类型施加 TTL 与容量限制

Store(存储层)

store 是磁盘上的 content-addressed blob 存储。每个 blob 按 SHA-256 内容哈希组织为目录,路径格式:{cacheDir}/{contentHash}/{filename}

store 不负责 TTL 与容量策略,仅负责文件级读写、摄入、删除。

参见 cache/store.go:11

方法 作用
Write []byte 写入指定内容哈希目录
Read 返回文件句柄(调用方负责关闭)
ReadBytes 读取全部内容并返回 []byte
Ingest 将现有临时文件移动(失败回退复制)到 store,适合大文件
Remove 按内容哈希删除整个 blob 目录

实现细节:

  • Ingest 优先 os.Rename(原子移动);失败时回退为 copy + delete。该设计可在同文件系统下以较低开销摄入大文件。参见 cache/store.go:65
  • sanitizeFilename 使用 filepath.Base 去除路径分量;containedUnder 通过绝对路径前缀检查避免路径穿越(path traversal)。参见 cache/store.go:106

Index / Manifest(索引层)

index 是内存中的缓存条目注册表,持久化文件为 cache.json(manifest)。存储格式为版本化 JSON:

{
  "version": 2,
  "entries": {
    "<canonical_key>": { ... }
  }
}

当前版本号:2cache/index.go:15)。

关键行为:

  • index.load() 启动时优先按 v2 格式解析(tryLoadV2)。
  • 若 manifest 不存在或版本不匹配,则调用 resetCache 清空后重建空索引。
  • 这意味着旧版本 manifest 不迁移,而是静默清空。参见 cache/index.go:35

持久化策略:

  • flush() 采用“临时文件写入 + os.Rename 原子替换”,保证崩溃安全写入。参见 cache/index.go:90

CacheEntry 字段定义(cache/domain.go:106):

字段 类型 说明
Kind EntryKind KindArtifactKindMetadata
Filename string store 内文件名
Size int64 字节大小
ContentHash string SHA-256 十六进制(content addressing 主键)
Integrity Integrity 上游哈希算法 + 预期值 + 实际值 + 验证状态
Expiration time.Time TTL 到期时间
Key string canonical URL 键
CreatedAt time.Time 写入时间戳

Policy(策略层)

PolicyKindMetadataKindArtifact 分别维护独立 PolicyConfig。每类均包含:

  • MaxSize:容量上限(字节)
  • TTL:生存时长

参见 cache/policy.go:13

// ../lucy/cache/policy.go:8-27
type PolicyConfig struct {
 MaxSize int64         `json:"max_size"`
 TTL     time.Duration `json:"ttl"`
}

type Policy struct {
 Metadata PolicyConfig `json:"metadata"`
 Artifact PolicyConfig `json:"artifact"`
}

func (p *Policy) ConfigFor(kind EntryKind) PolicyConfig {
 switch kind {
 case KindMetadata:
  return p.Metadata
 case KindArtifact:
  return p.Artifact
 default:
  return p.Artifact
 }
}

默认策略由 DefaultCacheConfig() 提供(cache/config.go:16):

类别 MaxSize TTL
Artifact(JAR、二进制) 2 GB 7 天
Metadata(版本清单、API 响应) 50 MB 4 小时

补充说明:

  • Policy.ConfigFor(kind)kind 路由策略。
  • Policy.Validate() 初始化时验证各字段均为正值,防止无效配置静默生效。

生命周期(Lifecycle)

初始化

cache.Network() 通过 sync.Once 调用 newHandler()cache/cache_handler.go:21),流程如下:

  1. setDir(name) 决定缓存目录:优先 os.UserCacheDir(),失败降级 os.TempDir();路径为 {userCacheDir}/{programName}/network
  2. os.MkdirAll(..., 0700) 创建目录;若失败,handler.on = false,缓存进入禁用状态。
  3. newIndex(manifestPath).load() 加载/初始化 manifest;若失败,同样禁用缓存。
  4. 初始化清理:先 clearExpiredCache(),再 maintainCacheLimit(),最后 flush() 落盘清理结果。

写入路径(Write Path)

写入存在两个入口:

  • AddEntry(data []byte, ...)(小文件/内存数据):

    • handler 计算 SHA-256 作为 ContentHash
    • 写入 store 并更新 index
    • 若同 key 已存在且哈希一致,则跳过(幂等)
    • 若哈希变化,则先删除旧 blob 再写入新 blob
    • 参见 cache/cache_handler.go:87
  • IngestEntry(srcPath string, ...)(大文件,如服务器 JAR):

    • ContentHash 由调用方预先计算
    • 直接 ingest 文件(优先 os.Rename),避免整文件载入内存
    • 参见 cache/cache_handler.go:158

两条路径最终都会:

  • 写入 CacheEntry 到 index
  • 立即调用 index.flush() 持久化

键规范化:写入前调用 canonicalizeKey(),执行 scheme/host 小写化、标准端口移除、query 参数排序、fragment 去除,确保语义等价 URL 命中同一条目(cache/cache_tools.go:80)。

读取路径(Read Path)

handler.Get(k)handler.GetBytes(k) 都在 sync.RWMutex 读锁下执行:

  1. canonicalizeKey(k) 规范化 key
  2. index.get(ckey) 查询条目
  3. miss 时返回 (false, nil, nil)(cache miss,非错误)
  4. hit 时通过 store.Read / store.ReadBytes 读取 blob;读取失败则返回错误

handler.Exist(k) 仅查 index,不访问磁盘,适用于快速命中判断。

[!WARNING] 读取路径不检查 Expiration。过期清理只在 newHandler() 初始化执行一次,因此两次启动之间即使条目已超 TTL,仍可能被读取命中。若需要严格 TTL 语义,调用方需自行检查 CacheEntry.Expiration

逐出(Eviction)

逐出仅在 newHandler() 初始化阶段执行(cache/cache_tools.go:27),包含两步:

  1. TTL 清理clearExpiredCache() 删除所有 Expiration.Before(time.Now()) 的条目(store + index 同步删除)。
  2. 容量维护maintainCacheLimit()Expiration 升序排序;若某类(Artifact/Metadata)总大小超过 Policy.MaxSize,则依次删除最旧条目,直到回到限制以内(近似 LRU-by-expiration)。参见 cache/cache_tools.go:43

[!WARNING] 无后台定时逐出任务。若 Lucy 长时间运行(如 daemon),缓存可能在运行期间持续增长,直到下次重启才触发清理。

手动清空(Manual Reset)

handler.ClearAll() 调用 resetCache()cache/cache_manifest.go:24):

  1. 删除 manifest 文件
  2. 遍历缓存目录并 os.RemoveAll 清理所有条目
  3. 重建空 manifest

该操作返回 ResetReport,包含 TotalFreedSizeFileCount

完整性验证(Integrity Verification)

Integritycache/domain.go:92)记录上游哈希信息与验证结果,支持算法:

算法 主要用途
HashSHA1 Mojang 上游 artifact 哈希
HashSHA256 Lucy 内部 content addressing
HashSHA512 Modrinth 上游 artifact 哈希

IntegrityState

  • IntegrityVerified:写入阶段已完成哈希验证
  • IntegrityUnverified:未提供预期摘要,仅尽力缓存

当调用方未提供预期哈希时,条目将以 IntegrityUnverified 存储,不提供完整性保证。

并发安全模型

handler 通过 sync.RWMutex 实现并发安全(cache/cache_handler.go:12):

  • 读锁:GetGetBytesExistAll
  • 写锁:AddEntryIngestEntryRemoveClearAllFlush

cache.Network() 使用 sync.Once 保证单例初始化的并发幂等。

已知缺口(Known Gaps)

Mojang 数据源尚未接入缓存

upstream/mojang/mojang.go 当前仅定义 VersionManifestURL未调用缓存层。结果是每次请求 Mojang 版本清单都会直连网络,不经过 cache.Network(),也不受 TTL/容量策略管理。

[!WARNING] 此功能尚未完整/正确实现 upstream/mojang/mojang.go 缺少缓存集成。Mojang 版本清单下载绕过缓存层,重复网络开销无法消除。

修复建议:在 Mojang provider 中加入 cache.Network().GetBytes() 命中检查;下载后使用 cache.Network().AddEntry()KindMetadata 写入,以默认 4 小时 TTL 减少重复请求。

运行期间无后台逐出

缓存逐出只在启动时执行一次;长生命周期进程在两次启动之间不会自动清理过期或超限数据。

[!WARNING] 此功能尚未完整/正确实现 缓存缺少周期性后台清理机制。daemon 等长运行场景下,缓存可能持续增长。

读取路径不检查过期

handler.Get() / handler.GetBytes() 不验证 CacheEntry.Expiration,因此“已过期但尚未被清理”的条目仍可命中。

[!WARNING] 该行为对短 TTL 的 Metadata 影响更明显;两次进程启动之间,可能命中过期元数据并返回陈旧版本清单。

相关页面

参考文件

本页依据以下源码(基线 commit:86d3480cffa821b9b9c0747263a637b2973a7366):

  • cache/cache.go:25Network() 单例入口,sync.Once 保证初始化幂等
  • cache/cache_handler.go:12handler 结构(storeindexpolicymu
  • cache/cache_handler.go:21newHandler() 初始化(目录、manifest、启动时逐出)
  • cache/cache_handler.go:87AddEntry() 内存写入路径(幂等检查、旧 blob 清理)
  • cache/cache_handler.go:158IngestEntry() 大文件写入路径(优先 os.Rename
  • cache/cache_handler.go:249Get() / GetBytes() 读路径(持读锁,不检查过期)
  • cache/store.go:11store content-addressed 存储定义
  • cache/store.go:65Ingest() 原子移动与 copy+delete 回退
  • cache/store.go:106sanitizeFilename() + containedUnder() 路径穿越防护
  • cache/index.go:15indexVersion = 2
  • cache/index.go:35index.load() 版本化加载与失败重建
  • cache/index.go:90index.flush() 原子替换写入
  • cache/cache_manifest.go:14manifestFilename = "cache.json"
  • cache/cache_manifest.go:24resetCache():清空 manifest + blob,返回 ResetReport
  • cache/policy.go:13Policy(Metadata / Artifact 独立配置)
  • cache/config.go:16DefaultCacheConfig()(Artifact 2 GB/7 天;Metadata 50 MB/4 小时)
  • cache/domain.go:11EntryKindKindMetadata / KindArtifact
  • cache/domain.go:30HashAlgorithm(SHA1/256/512)及上游对应
  • cache/domain.go:92Integrity 结构体
  • cache/domain.go:106CacheEntry 字段定义
  • cache/cache_tools.go:27clearExpiredCache()(TTL 清理)
  • cache/cache_tools.go:43maintainCacheLimit()(按过期时间升序逐出)
  • cache/cache_tools.go:80canonicalizeKey()(URL 规范化:小写、排序、去 fragment)
  • upstream/mojang/mojang.go — 仅有 VersionManifestURL未集成缓存