Files
soft_quay/docs/tasks/T-606.md
T

98 lines
8.8 KiB
Markdown
Raw Normal View History

2026-07-17 09:10:20 +08:00
---
id: T-606
title: 收紧图标缓存并发与内存边界
phase: 2
deps: [T-204, T-605]
2026-07-17 09:38:25 +08:00
status: DONE
2026-07-17 09:10:20 +08:00
created: 2026-07-17
issue: null
2026-07-17 09:38:25 +08:00
context_ref: 8873a5261db76b30db30521197e9ec898ca86077
2026-07-17 09:10:20 +08:00
claim_branch: null
2026-07-17 09:38:25 +08:00
work_branch: agent/codex/T-606
2026-07-17 09:10:20 +08:00
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 09:38:25 +08:00
- 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 构建。