cache system - mclucy/lucy GitHub Wiki
缓存系统(Cache System)
本文系统化说明 Lucy cache 包的实现与行为,覆盖:
- 三层架构职责(
store/index/policy) - 缓存条目完整生命周期(初始化、写入、读取、逐出、清空)
- 完整性校验与并发安全模型
- 当前已知集成缺口与运行时限制
缓存入口为 cache.Network():该函数返回进程级单例 handler,所有 HTTP 下载通过该 handler 进行 content-addressed 缓存。
KindArtifact:JAR、二进制等大文件,长 TTLKindMetadata:版本清单、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
}
关键要点
架构与数据模型
缓存由三个内部层协同构成:
- Store(存储层):管理 content-addressed blob 的磁盘 I/O
- Index / Manifest(索引层):管理缓存条目映射并持久化清单
- 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>": { ... }
}
}
当前版本号:2(cache/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 |
KindArtifact 或 KindMetadata |
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(策略层)
Policy 为 KindMetadata 与 KindArtifact 分别维护独立 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),流程如下:
setDir(name)决定缓存目录:优先os.UserCacheDir(),失败降级os.TempDir();路径为{userCacheDir}/{programName}/network。os.MkdirAll(..., 0700)创建目录;若失败,handler.on = false,缓存进入禁用状态。newIndex(manifestPath).load()加载/初始化 manifest;若失败,同样禁用缓存。- 初始化清理:先
clearExpiredCache(),再maintainCacheLimit(),最后flush()落盘清理结果。
写入路径(Write Path)
写入存在两个入口:
-
AddEntry(data []byte, ...)(小文件/内存数据):- handler 计算 SHA-256 作为
ContentHash - 写入 store 并更新 index
- 若同 key 已存在且哈希一致,则跳过(幂等)
- 若哈希变化,则先删除旧 blob 再写入新 blob
- 参见
cache/cache_handler.go:87
- handler 计算 SHA-256 作为
-
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 读锁下执行:
canonicalizeKey(k)规范化 keyindex.get(ckey)查询条目- miss 时返回
(false, nil, nil)(cache miss,非错误) - hit 时通过
store.Read/store.ReadBytes读取 blob;读取失败则返回错误
handler.Exist(k) 仅查 index,不访问磁盘,适用于快速命中判断。
[!WARNING] 读取路径不检查
Expiration。过期清理只在newHandler()初始化执行一次,因此两次启动之间即使条目已超 TTL,仍可能被读取命中。若需要严格 TTL 语义,调用方需自行检查CacheEntry.Expiration。
逐出(Eviction)
逐出仅在 newHandler() 初始化阶段执行(cache/cache_tools.go:27),包含两步:
- TTL 清理:
clearExpiredCache()删除所有Expiration.Before(time.Now())的条目(store + index 同步删除)。 - 容量维护:
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):
- 删除 manifest 文件
- 遍历缓存目录并
os.RemoveAll清理所有条目 - 重建空 manifest
该操作返回 ResetReport,包含 TotalFreedSize 与 FileCount。
完整性验证(Integrity Verification)
Integrity(cache/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):
- 读锁:
Get、GetBytes、Exist、All - 写锁:
AddEntry、IngestEntry、Remove、ClearAll、Flush
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:25—Network()单例入口,sync.Once保证初始化幂等cache/cache_handler.go:12—handler结构(store、index、policy、mu)cache/cache_handler.go:21—newHandler()初始化(目录、manifest、启动时逐出)cache/cache_handler.go:87—AddEntry()内存写入路径(幂等检查、旧 blob 清理)cache/cache_handler.go:158—IngestEntry()大文件写入路径(优先os.Rename)cache/cache_handler.go:249—Get()/GetBytes()读路径(持读锁,不检查过期)cache/store.go:11—storecontent-addressed 存储定义cache/store.go:65—Ingest()原子移动与 copy+delete 回退cache/store.go:106—sanitizeFilename()+containedUnder()路径穿越防护cache/index.go:15—indexVersion = 2cache/index.go:35—index.load()版本化加载与失败重建cache/index.go:90—index.flush()原子替换写入cache/cache_manifest.go:14—manifestFilename = "cache.json"cache/cache_manifest.go:24—resetCache():清空 manifest + blob,返回ResetReportcache/policy.go:13—Policy(Metadata / Artifact 独立配置)cache/config.go:16—DefaultCacheConfig()(Artifact 2 GB/7 天;Metadata 50 MB/4 小时)cache/domain.go:11—EntryKind(KindMetadata/KindArtifact)cache/domain.go:30—HashAlgorithm(SHA1/256/512)及上游对应cache/domain.go:92—Integrity结构体cache/domain.go:106—CacheEntry字段定义cache/cache_tools.go:27—clearExpiredCache()(TTL 清理)cache/cache_tools.go:43—maintainCacheLimit()(按过期时间升序逐出)cache/cache_tools.go:80—canonicalizeKey()(URL 规范化:小写、排序、去 fragment)upstream/mojang/mojang.go— 仅有VersionManifestURL,未集成缓存