Files
minio/docs/investigations/issue-77/review/opus5-max-v2.md
2026-09-12 17:52:25 +08:00

45 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SILO #77 修复计划 v2 对抗复审(Claude Code Opus 5,只读)
基准 worktree = main `5c576581631561c446f30ae5b566f0aa793adc1c`。本轮只针对 v1→v2 的修订差异做源码核验:未改产品代码、未运行测试、未调用其他代理。
## 首轮三个阻断的关闭判断
**R1(零配额发送端改写 / 落盘≠出站):已关闭。** v2 取消 `admin-bucket-handlers.go:97-99` 的 `Quota=nil` 改写后,`mc quota clear` 两侧都落 live 零值文档:`bucket-quota.go:99-110` 对 `{}`/`null` 均解析为零值且 `IsValid()` 为真,`isBktQuotaCfgReplicated`(`site-replication.go:3850-3888`)此时 `numquotaCfgs==total`、Type 相同 → 判为已复制,`quotaCfgSet`(`3635`)仍为 false,#91 计数不受影响。接收端 `PeerBucketQuotaConfigHandler:2089` 会重新 `json.Marshal` 导致字节不同,但 v2 §2 规定 Quota 比较键取"解析结果的 JSON 编码",`madmin.BucketQuota` 是定长结构体、编码确定,不会因此空转。Policy 侧改成本地/导入/bulk 统一按 `IsEmpty()` 删除,与既有 `admin-handlers-site-replication.go:233-237` 一致,三元组不变量成立。
**R2(saveMetadata 静默归一 / §6 不可实现 / 字节序方向未定):已关闭。** `saveMetadata`(`bucket-metadata-sys.go:232`)全部 6 个调用点(`site-replication.go:961/1708/2049`、`bucket-metadata-sys.go:203/222/354`、`admin-bucket-handlers.go:1095`)传入的都是调用方自有值,改指针回写无别名风险;导入 hook 改用提交快照后,`1110-1127` 与落盘文档不再分叉;比较改用 `parseAllConfigs:391-399` 同一条 Object Lock→Enabled 归一规则,方向也已写明"键较大者胜"且禁止用字节序补偿隐式改写。
**R3(bulk 删除语义):已关闭,且首轮依据确属错误。** 复核确认:`json.RawMessage` 显式 `null` 解码为非 nil 的 `[]byte("null")`(探针 log 第 3-4 行),`policy/bucket-policy.go:78` 允许 `Version==""`,故 `null` 与 `{"Version":...,"Statement":[]}` 都能通过 `ParseBucketPolicyConfig` 且 `IsEmpty()` 为真;`*string` 的 `null` 才退化成 nil。v2 的逐字段规则与 `PeerBucketMetadataUpdateHandler:1653-1697` 的 `!=nil` 约定一致,也堵住了"`len==0 ⇒ 删除"的错删诱导。唯一 bulk 生产者(导入 `1103-1131`)在 `omitempty` 下漏发空 Policy 的问题,v2 用专用 nil 事件补齐,处置正确。
## P1(新增阻断,1 项)
**P1-1:`fieldTime <= Created` 一律判 baseline,会让历史桶的配置在初次同步中被静默丢弃且 heal 永不修复。**
反例(全部为已修复版本、同一桶世代、合法带时间事件,属计划承诺范围):
1. A 站点有 2020 年前迁移来的桶。`applyLegacyConfigs`(`bucket-metadata.go:482-513`)不写任何 `*UpdatedAt`,`convertLegacyConfigs` 保存后磁盘上这些字段仍是零值;每次 `loadBucketMetadataParse:236-238` → `defaultTimestamps:534-561` 把 `PolicyConfigUpdatedAt` 补成 **正好等于 Created**。
2. 运维执行 `AddPeerClusters`。`MakeBucketHook:821-823` 把 `createdAt` 传给对端,`PeerBucketMakeWithVersioningHandler:950` 的 `SetCreatedAt` 使 B 的 `Created` 与 A **完全相等**。
3. `syncToAllPeers:2217-2228` 发出 Policy@Created(非零,因此不走 legacy-zero 例外)。
4. 按 v2 §3"非零事件**不晚于**目标桶 Created 时成功/no-op,记录 before-created" → B 直接 no-op,policy 落不下去。
5. 按 v2 §2/提交 2.1,A 的候选是 baseline → 过滤 → "无候选,显式 found=false" → 永远不 heal,只产出 indeterminate。
主干今天这条路径是成功的(`PeerBucketPolicyHandler:1718-1746` 无 before-created 检查,直接应用),bulk 侧 `1646` 也只拒绝 **严格早于** Created。因此这是回归,不是遗留缺陷;Tags/SSE/Quota 同理,Object Lock 因 `955-959` 自举侥幸掩盖。T1–T9 没有"字段时间等于 Created 的历史桶初次同步"用例,实现期不会被发现。
最小修正(两句契约 + 一行验收):①把 apply 侧边界改成**严格早于** Created 才 before-created;`fieldTime == Created` 保留 baseline 的"弱"语义。②baseline 只是排序上低于任何非 baseline 候选,不是"不可用":目标该字段自身也是 baseline/缺失时,baseline-live 仍可确立初值,仅在存在非 baseline 候选时被压制;全 baseline 且内容不同时按既定键排序取胜者,而不是报 indeterminate。③T7/T8 增加一行"全部字段时间等于 Created 的历史桶,初次同步与一轮 heal 后各站点一致"。
## P2(仍需改动)
- **Policy 比较键的构造方式。** 递归排序自造规范 JSON 需要覆盖 `Condition` 的 `map[string]map[string]ValueSet`、`NotAction/NotResource/NotPrincipal`,一旦两站点对同一策略算出不同键,同时间平局会各自选出不同胜者 → 每轮互推、不收敛。建议判等直接复用现成的 `BucketPolicy.Equals`(`site-replication.go:3912` 已在用),规范串只用于同时间平局排序,并复用 `ActionSet.String()`(silo-pkg `policy/actionset.go:151-158`,已排序)。同时把"键必须是**已解析策略**的纯函数、跨站点必须一致"写成不变量,T2 补一条"两站点各自字节 → 相同键"断言。
- **开关语义与 Policy 的实际不对称需写进文档。** `site-replication.go:4069-4070` 无条件导出 `PolicyUpdatedAt`,因此提交 1/2 落地后,**开关 off 时 Policy 删除照样会经 heal 传播**,只有 Tags/SSE/Quota 被门控。v2 表格里"初次同步的真实删除:off 保留已有行为"容易被读成"off 完全不传播删除",运维会据此做错误的升级判断。
- **off 模式下的稳定态 RPC 要写进验收预期。** 持有 Tags/SSE/Quota 墓碑的站点在 off 下导出不带时间 → 每轮仍被选为待修复目标 → 每 30s(`4651`)一次必然被拒的 RPC。提交 2 的"对已归一且相同的目标不写入、不发 RPC"不覆盖该情形。请明确这是 off 期间的预期代价,并禁止为它加每轮日志(会与 §3 的三类有界日志冲突)。
- **空策略 PUT 的对外行为补一条断言。** v2 让本地 `PutBucketPolicy` 对 `{"Version":"2012-10-17","Statement":[]}`(`bucket-policy-handlers.go:98-101` 只拒空 Version,该文档合法)改走删除。T1 请断言"PUT 返回成功、GET 返回 NotFound、出站专用事件与落盘一致",避免实现时误改成 4xx。
## 作者补查核验
**S1 成立。** `internal/logger/logonce.go:109` 仅在 `prev.Err.Error() == nerr.Error()` 时抑制;正文含变化时间会每轮重新打印(且分支不更新 map,等于完全失效)。固定正文 + ReqInfo 的做法正确。
**S2 成立。** `policy/actionset.go:144-168` 的 `MarshalJSON → ToSlice` 直接遍历 map 不排序,`json.Marshal(BucketPolicy)` 确实不稳定,v1 的字节比较键不可用;v2 另建比较键的结论正确(构造方式见上 P2)。
---
**结论:NO_GO。实施前阻断数:1(P1-1)。** 首轮 R1/R2/R3 三项均已实质关闭,R4–R10、O1–O6 的处置未引入新的明确阻断;余下 4 条 P2 建议与 P1-1 一并修订即可进入实现,不需要再做全量复审。