--- id: T-606 title: 收紧图标缓存并发与内存边界 phase: 2 deps: [T-204, T-605] status: DONE created: 2026-07-17 issue: null context_ref: 8873a5261db76b30db30521197e9ec898ca86077 claim_branch: null work_branch: agent/codex/T-606 write_paths: - docs/tasks/T-606.md - core/catalog/ - app-modern/ui/gio/ - app-win7/ui/gio/ - docs/api.md - docs/routes.md - docs/04-architecture.md - docs/05-coding-rules.md - docs/review/phase2-review.md - docs/00-ai-start-here.md - docs/06-tasks.md - docs/current-state.md --- ## 问题 / 背景 Phase 2 交叉审核确认,T-204 的 `IconCache.Load` 从取得全局 mutex 到返回全程持锁,锁内包含磁盘读取、图片校验、远端 Fetch 和磁盘原子写。一个慢图标会阻塞所有其他 key,包括本应立即返回的内存命中;简单地把 Fetch 移出锁又会使同一 key 重复下载并竞争写盘。 现有 `IconFetcher` 直接返回完整 `[]byte`,`IconCache` 只能在 Fetch 完成后检查 2 MiB 上限。该检查能阻止超大对象进入缓存,却不能阻止未来网络 Fetcher 先下载并分配任意大响应。与此同时,`IconCache.memory` 没有容量/LRU 上限,modern/win7 的 `shell.icons` 也不会在 Catalog 移除 app 后剪枝,长期运行时内存只增不减。 当前 `IconCache.Load` 尚无生产调用者,T-204 也明确把真实图标分发映射与网络装配留给后续,所以这些不是已经复现的滚动验收失败。它们仍是正式图标后台加载接入前必须关闭的并发与资源边界。 ## 方案 1. 把 `IconFetcher` 调整为可有界读取的流式响应合约,由 `IconCache` 掌握读取边界与关闭责任: - 响应提供 `io.ReadCloser` body 和可选的声明长度;未知长度使用明确哨兵值。 - 声明长度超过 `maxBytes` 时在读取 body 前拒绝。 - 对所有响应使用 `io.LimitReader(maxBytes+1)` 或等价逻辑;实际读到上限加一字节时返回 `ErrIconTooLarge`。 - 成功、校验失败、读取失败、context 取消等所有路径都关闭 body,并保留关闭错误的既定错误优先级。 - 不在本任务猜测 Catalog 内容哈希到 URL/DPI 变体的映射,也不实现具体发布端协议;后续 HTTP 适配器必须使用该流式合约。 2. 为 `IconCache` 建立 Go 1.20 兼容的按 cache key in-flight 去重: - 全局 mutex 只保护 memory/LRU/in-flight 元数据,不横跨磁盘、网络、图片解码或磁盘写入。 - 同一 digest+DPI 只允许一个 leader 执行 disk→fetch→validate→store,其余调用者等待并复用结果。 - 不同 key 可以并行,慢 key 不阻塞另一 key 的 memory hit。 - follower 的 context 取消只停止自身等待,不得取消 leader;leader 失败/取消后发布同一结果、移除 flight,后续调用可重试。 - 每个调用者取得独立的结果字节副本,不能修改 cache 或其他等待者观察到的内容。 3. 将内存层改为按 key 的 LRU,同时限制总字节与条目数: - 默认上限冻结为 32 MiB、256 个 key;命中提升 recency,插入/替换时更新准确字节计数并持续淘汰最旧项直到两个上限同时满足。 - 同一 digest 的不同 DPI 仍是不同 key;磁盘缓存与现有内容校验/原子写语义不变。 - LRU 操作与 in-flight 完成发布在同一同步纪律下,不得产生负计数、重复 list 节点或返回已被复用的内部切片。 4. modern/win7 的 `AppShell.SetItems` 按新 Catalog app ID 集合剪枝 `shell.icons`,保留仍存在 ID 的 `paint.ImageOp`;与现有 rows/categories 剪枝语义对齐。 5. 扩展并发、流边界、LRU 与双 Gio 测试,使用 channel/barrier 等确定性同步,不以 `time.Sleep` 猜测并发时序。 6. 同步图标缓存协议、架构与编码规则,明确“内存→磁盘→流式 Fetcher”的边界、内存容量、按 key 去重以及真实 UI 接入仍须走 application event/UI goroutine。 ## 验收要点 - 一个阻塞的远端 key 不会阻塞另一 key 的内存命中;两个不同远端 key 能实际重叠执行。 - 同一 key 的并发 miss 只执行一次 Fetch 和一次成功存储,所有等待者获得内容相同但 backing array 独立的结果。 - follower context 取消能及时返回且不影响 leader;leader 失败或取消后 in-flight 被清理,下一次调用可以重新 Fetch。 - 声明 `Content-Length` 超过 2 MiB 默认上限时不读取 body;未知/伪造较小长度但实际 body 超限时最多读取 `maxBytes+1`;所有路径关闭 body。 - 哈希不符、非法图片、磁盘损坏在线修复、离线读取已验证磁盘缓存、DPI 分键和非致命磁盘写 warning 的既有语义保持通过。 - 默认 memory LRU 同时受 32 MiB 与 256 key 限制;命中提升顺序,替换正确更新字节数,淘汰后可从磁盘/远端重新装入。 - modern/win7 `SetItems` 删除 app 后对应 `shell.icons` 项消失,保留 app 的 ImageOp 不变;rows/categories 既有稳定性测试继续通过。 - 并发测试使用确定性同步并可重复运行;若当前工具链支持 race detector,执行 `go test -race` 并记录结果,不把 race 支持作为 Win7 交叉构建的前提。 - Go 1.20.14 + `GOWORK=off` 下 `go vet ./...`、`go test -count=1 ./...` 通过。 - modern 根 workspace 与 win7 独立 workspace 的 `ui/gio` 测试和 Windows amd64 构建通过;`./scripts/verify_phase0.ps1` 全绿。 - `python scripts/validate_agent_context.py`、`python scripts/validate_harness_governance.py` 与提交前差异检查通过。 ## 边界(不改什么) - 不决定 `sha256:` 图标引用到下载 URL、CDN 路径或 DPI 变体的发布端映射,不增加未经协议冻结的 Catalog 字段。 - 不把 `IconCache` 正式装配进应用启动或列表滚动链路;后台事件→UI goroutine→`ApplyIcon`/Invalidate 的接线按审核顺序另立后续任务。 - 不补 modern/win7 完整适配器交互契约测试,不修改 `VisibleItems` 快照 API,不拆分 `shell.go`;这些按审核后续顺序分别处理。 - 不对磁盘图标缓存实现 LRU/容量清理,不自动删除或 quarantine symlink/reparse point;`ErrIconCacheUnsafe` 继续 fail closed。 - 不引入 `x/sync/singleflight` 或其他第三方缓存库,不升级 Go/Gio,core 与 win7 继续保持 Go 1.20 兼容。 - 不处理 ZIP 中央目录预扫描、断电耐久、Catalog 签名向量或 T-302 安装整合。 - 不修改、提交或删除用户的 `soft_quay.code-workspace`。 ## 协作约束 - 按仓库当前规则由单 Agent 串行执行,不启动子 Agent。 - 本任务只允许修改 frontmatter 中的 `write_paths`;若流式 Fetcher 需要新增公共协议字段或真实 URL 规则,必须停止并记录,不得在 T-606 内猜测协议。 - T-606 完成、完整验证并提交前,不落成或领取 Phase 2 审核顺序中的后续任务。 ## 执行记录 - 2026-07-17:根据 `docs/review/phase2-review.md` 交叉复核定稿的第一优先级整改落成任务;现有全局最大任务为 T-605,因此取 T-606。 - 2026-07-17:任务合并收口按 key in-flight、流式有界读取、memory LRU 与双 shell 图标剪枝;UI 线程投递、适配器契约、VisibleItems 快照和文件拆分继续按审核顺序串行拆分。 - 2026-07-17:在 `agent/codex/T-606` 分支领取任务,基线为 `8873a5261db76b30db30521197e9ec898ca86077`;保持单 Agent 串行执行。 - 2026-07-17:`IconFetcher` 改为返回 context-bound `io.ReadCloser` 与可选声明长度;`IconCache` 在分配完整响应前拒绝超大声明并只读取 `maxBytes+1`,成功、超限、取消和校验失败路径统一关闭 body。未增加 URL/CDN 字段或具体 HTTP 映射。 - 2026-07-17:`IconCache` 全局 mutex 收缩为只保护 memory/LRU/in-flight 元数据;同 digest+DPI 使用单 leader,不同 key 的磁盘/网络工作可并行,follower 可独立取消,leader 失败后 flight 清理并允许重试。每个调用者与 LRU 都持有独立字节副本。 - 2026-07-17:新增默认 32 MiB/256-key 的双上限 LRU,覆盖命中提升、替换计数、条目/字节淘汰、禁用边界与淘汰后 verified disk 重载;modern/win7 `SetItems` 同步剪枝已移除 app 的 ImageOp 并保留现存 app 图标。 - 2026-07-17:定向验证通过:Go 1.20.14 `go vet ./catalog`、`go test -count=20 ./catalog`;modern/win7 `go test -count=5 ./ui/gio`。并发用例使用 channel/barrier 与 2 秒仅防挂死超时,覆盖跨 key 并行、memory hit 不受慢 key 阻塞、同 key 单 Fetch、follower/leader 取消、flight 重试、body 最大读取量和独立 backing array。 - 2026-07-17:尝试 Go 1.20.14 `go test -race -count=1 ./catalog`;当前 Windows 环境缺少 GCC(`cgo: C compiler "gcc" not found`),race detector 不可用。按任务边界记录限制,未把它伪报为通过;确定性并发测试连续 20 次通过。 - 2026-07-17:完整 `./scripts/verify_phase0.ps1` 通过,包含治理/上下文/边界/版本校验、Go 1.20.14 core vet/test、modern Go 1.25 与 win7 Go 1.20.14 的 UI/平台测试及 Windows amd64 构建。