mirror of
https://github.com/pgsty/minio.git
synced 2026-09-26 04:45:59 +03:00
docs: archive issue 77 design, adversarial reviews and acceptance evidence
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
Testing builds for OS/Arch: linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64
|
||||
--> linux/amd64:github.com/minio/minio
|
||||
--> linux/arm64:github.com/minio/minio
|
||||
--> darwin/amd64:github.com/minio/minio
|
||||
--> darwin/arm64:github.com/minio/minio
|
||||
--> windows/amd64:github.com/minio/minio
|
||||
--> windows/arm64:github.com/minio/minio
|
||||
@@ -0,0 +1,78 @@
|
||||
ok github.com/minio/minio/internal/amztime 5.149s
|
||||
ok github.com/minio/minio/internal/arn 13.830s
|
||||
ok github.com/minio/minio/internal/auth 5.472s
|
||||
ok github.com/minio/minio/internal/bpool 13.244s
|
||||
ok github.com/minio/minio/internal/bucket/bandwidth 12.671s
|
||||
ok github.com/minio/minio/internal/bucket/cors 2.705s
|
||||
ok github.com/minio/minio/internal/bucket/encryption 12.228s
|
||||
ok github.com/minio/minio/internal/bucket/lifecycle 3.223s
|
||||
ok github.com/minio/minio/internal/bucket/object/lock 11.599s
|
||||
ok github.com/minio/minio/internal/bucket/replication 12.817s
|
||||
ok github.com/minio/minio/internal/bucket/versioning 13.157s
|
||||
ok github.com/minio/minio/internal/cachevalue 18.526s
|
||||
? github.com/minio/minio/internal/color [no test files]
|
||||
ok github.com/minio/minio/internal/config 14.365s
|
||||
? github.com/minio/minio/internal/config/api [no test files]
|
||||
? github.com/minio/minio/internal/config/batch [no test files]
|
||||
? github.com/minio/minio/internal/config/browser [no test files]
|
||||
? github.com/minio/minio/internal/config/callhome [no test files]
|
||||
ok github.com/minio/minio/internal/config/compress 15.754s
|
||||
ok github.com/minio/minio/internal/config/dns 17.351s
|
||||
? github.com/minio/minio/internal/config/drive [no test files]
|
||||
ok github.com/minio/minio/internal/config/etcd 18.460s
|
||||
? github.com/minio/minio/internal/config/heal [no test files]
|
||||
ok github.com/minio/minio/internal/config/identity/ldap 19.132s
|
||||
ok github.com/minio/minio/internal/config/identity/openid 19.829s
|
||||
? github.com/minio/minio/internal/config/identity/openid/provider [no test files]
|
||||
? github.com/minio/minio/internal/config/identity/plugin [no test files]
|
||||
? github.com/minio/minio/internal/config/identity/tls [no test files]
|
||||
ok github.com/minio/minio/internal/config/ilm 19.256s
|
||||
? github.com/minio/minio/internal/config/lambda [no test files]
|
||||
ok github.com/minio/minio/internal/config/lambda/event 18.225s
|
||||
? github.com/minio/minio/internal/config/lambda/target [no test files]
|
||||
ok github.com/minio/minio/internal/config/notify 17.465s
|
||||
? github.com/minio/minio/internal/config/policy/opa [no test files]
|
||||
? github.com/minio/minio/internal/config/policy/plugin [no test files]
|
||||
? github.com/minio/minio/internal/config/scanner [no test files]
|
||||
ok github.com/minio/minio/internal/config/storageclass 17.289s
|
||||
ok github.com/minio/minio/internal/config/subnet 17.168s
|
||||
ok github.com/minio/minio/internal/crypto 18.081s
|
||||
ok github.com/minio/minio/internal/deadlineconn 21.703s
|
||||
ok github.com/minio/minio/internal/disk 17.514s
|
||||
ok github.com/minio/minio/internal/dsync 154.956s
|
||||
ok github.com/minio/minio/internal/etag 17.755s
|
||||
ok github.com/minio/minio/internal/event 17.598s
|
||||
ok github.com/minio/minio/internal/event/target 16.108s
|
||||
ok github.com/minio/minio/internal/grid 22.027s
|
||||
ok github.com/minio/minio/internal/handlers 14.030s
|
||||
ok github.com/minio/minio/internal/hash 14.016s
|
||||
? github.com/minio/minio/internal/hash/sha256 [no test files]
|
||||
ok github.com/minio/minio/internal/http 13.661s
|
||||
? github.com/minio/minio/internal/init [no test files]
|
||||
ok github.com/minio/minio/internal/ioutil 14.637s
|
||||
ok github.com/minio/minio/internal/jwt 13.120s
|
||||
ok github.com/minio/minio/internal/kms 12.536s
|
||||
ok github.com/minio/minio/internal/lock 13.029s
|
||||
ok github.com/minio/minio/internal/logger 12.231s
|
||||
? github.com/minio/minio/internal/logger/message/audit [no test files]
|
||||
? github.com/minio/minio/internal/logger/target/console [no test files]
|
||||
? github.com/minio/minio/internal/logger/target/http [no test files]
|
||||
? github.com/minio/minio/internal/logger/target/kafka [no test files]
|
||||
? github.com/minio/minio/internal/logger/target/loggertypes [no test files]
|
||||
? github.com/minio/minio/internal/logger/target/testlogger [no test files]
|
||||
ok github.com/minio/minio/internal/lsync 22.092s
|
||||
? github.com/minio/minio/internal/mcontext [no test files]
|
||||
? github.com/minio/minio/internal/mountinfo [no test files]
|
||||
? github.com/minio/minio/internal/net [no test files]
|
||||
? github.com/minio/minio/internal/once [no test files]
|
||||
ok github.com/minio/minio/internal/pubsub 11.507s
|
||||
ok github.com/minio/minio/internal/rest 10.318s
|
||||
ok github.com/minio/minio/internal/ringbuffer 10.927s
|
||||
ok github.com/minio/minio/internal/s3select 8.665s
|
||||
ok github.com/minio/minio/internal/s3select/csv 8.862s
|
||||
ok github.com/minio/minio/internal/s3select/json 8.547s
|
||||
ok github.com/minio/minio/internal/s3select/jstream 8.936s
|
||||
? github.com/minio/minio/internal/s3select/parquet [no test files]
|
||||
? github.com/minio/minio/internal/s3select/simdj [no test files]
|
||||
ok github.com/minio/minio/internal/s3select/sql 8.637s
|
||||
ok github.com/minio/minio/internal/store 9.594s
|
||||
@@ -0,0 +1,13 @@
|
||||
Installing golangci-lint v2.13.1
|
||||
golangci/golangci-lint info checking GitHub for tag 'v2.13.1'
|
||||
golangci/golangci-lint info found version: 2.13.1 for v2.13.1/darwin/arm64
|
||||
golangci/golangci-lint info installed .bin/golangci/v2.13.1/golangci-lint
|
||||
Running lint check
|
||||
0 issues.
|
||||
typos binary is not found.. skipping..
|
||||
LINT_EXIT=0
|
||||
compatibility manifest: imports=119 env=438 metrics=19 headers=87 routes=223 roots=1 grid=3 storage=15 policy=58 brand=181 sha256=db0d00c26360412a5388be1eb87c2dd763633ae243c558015f7b3ae86c80baaf
|
||||
Silo rebrand compatibility baseline is unchanged
|
||||
Silo delivery and runtime rebrand checks passed
|
||||
docker entrypoint argv compatibility tests passed
|
||||
REBRAND_EXIT=0
|
||||
@@ -0,0 +1,7 @@
|
||||
ok github.com/minio/minio/internal/s3select 2.150s
|
||||
ok github.com/minio/minio/internal/s3select/csv 3.612s
|
||||
ok github.com/minio/minio/internal/s3select/json 2.105s
|
||||
ok github.com/minio/minio/internal/s3select/jstream 2.937s
|
||||
? github.com/minio/minio/internal/s3select/parquet [no test files]
|
||||
? github.com/minio/minio/internal/s3select/simdj [no test files]
|
||||
ok github.com/minio/minio/internal/s3select/sql 2.620s
|
||||
@@ -0,0 +1,2 @@
|
||||
Silo delivery and runtime rebrand checks passed
|
||||
docker entrypoint argv compatibility tests passed
|
||||
@@ -0,0 +1,292 @@
|
||||
# SILO #77 实现深度对抗性代码审查报告
|
||||
|
||||
审查对象 `4089113e3`(基线 `5c5765816`)。只读 worktree,全部结论来自直接阅读 `cmd/` 生产代码、`silo-pkg`/`madmin-go` 依赖源码与调用链,并与证据目录交叉核对。测试 PASS 仅当作"已覆盖的观察",不作正确性证明。
|
||||
|
||||
> 说明:本会话的 Write/ExitPlanMode 工具不可用,报告直接输出于此,未落盘。
|
||||
|
||||
---
|
||||
|
||||
## 0. Verdict
|
||||
|
||||
**GO_WITH_NONBLOCKING_NOTES**
|
||||
|
||||
- **无条件阻断项:0**
|
||||
- **条件性阻断项:1**(F1)——条件:支持矩阵中存在 `.metadata.bin` 缺失或 `Created == 0` 的桶
|
||||
- 其余:F2 (S2)、F3 (S3)、F4 (S3)、F5(最小性)、F6(nit)、F7(范围说明)、F8(证据不足)、F9/F10(残留边界与覆盖缺口)
|
||||
|
||||
我逐条读了六类配置在全部入口的生产路径,**没有**找到在"同一桶世代、Created 已知、合法带源时间事件"范围内会发生回退、删除复活、锁外判旧或重复广播的反例。核心机制是正确的。缺陷集中在两处边缘:零 Created 历史桶被新代码判为不可写(F1,且其自带测试用 stub 掩盖了生产行为),以及 Policy 规范编码排序与既有公开 mismatch 统计口径冲突(F2)。
|
||||
|
||||
---
|
||||
|
||||
## 1. Findings
|
||||
|
||||
### F1 — 零 Created / 无 metadata.bin 的桶:六类配置写入全部硬失败;设计中的"物理桶 Created 补齐"在生产中不可达;配套测试用 stub 掩盖
|
||||
|
||||
**严重度 S1(条件性阻断)/ 否则 S2 · 已证实(源码级链路完整),需运行定量确认**
|
||||
|
||||
位置:
|
||||
- `cmd/bucket-metadata-replication.go:265-278` `ensureBucketMetadataCreated`
|
||||
- `cmd/bucket-metadata-sys.go:181-185`(六类分支入口,失败即 `return err`)
|
||||
- `cmd/erasure-server-pool.go:2280-2285` —— **`GetBucketInfo` 无条件用缓存 `meta.Created` 覆盖物理卷 Created**
|
||||
- `cmd/admin-bucket-handlers.go:1092`(import)、`cmd/site-replication.go:1671`(bulk peer apply)
|
||||
- `cmd/site-replication.go:2156-2166` + `cmd/site-replication-metadata.go:45-55`(初次同步静默跳过)
|
||||
- `cmd/site-replication-metadata-gate_test.go:145-159`(stub)
|
||||
|
||||
最短触发链:
|
||||
1. 桶存在,但 `.minio.sys/buckets/<b>/.metadata.bin` 不存在且无 legacy 配置文件 → `loadBucketMetadataParse` 走 `errConfigNotFound` 分支并**返回 nil error 且 `Created == 0`**(`cmd/bucket-metadata.go:230-297`;注意 `defaultTimestamps()` 只在 `err == nil` 时调用)。
|
||||
2. 启动时 `concurrentLoad`(`cmd/bucket-metadata-sys.go:737-763`)把这个 `Created == 0` 的 meta 写进 `metadataMap`。
|
||||
3. `PUT ?tagging`(或 policy/sse/quota/versioning/object-lock)→ `updateAndParseMetadata` → `ensureBucketMetadataCreated` → `objAPI.GetBucketInfo(..., NoMetadata:true)` → `erasureServerPools.GetBucketInfo` 先取到物理卷时间,**随后被缓存里的 0 覆盖** → `info.Created.IsZero()` → `errors.New("bucket metadata creation time is unknown")`。
|
||||
4. 客户端收到错误。基线版本此处成功(直接赋值 + `saveMetadata`)。
|
||||
|
||||
注:`getAllLegacyConfigs` 在 `cmd/bucket-metadata.go:468` 会用 `info.ModTime` 填 `Created`,所以**有** legacy 配置文件的桶反而没事;**从未设置过任何桶配置**的老桶才是高发场景。
|
||||
|
||||
用户影响:
|
||||
- 六类配置 PUT/DELETE 全部返回错误,**无运维恢复路径**;`mc admin bucket import` 对该桶整体失败(`rpt.SetStatus(bucket, "", err)`);peer bulk apply 返回错误导致该桶复制持续报错。
|
||||
- `syncToAllPeers` 对这类桶**静默跳过全部五类配置**(`state.candidate()` 要求 `valid`,而 `valid = !created.IsZero() && ...`),且**无任何诊断日志** —— 基线版本会发送。
|
||||
|
||||
最小修复(推荐 a):
|
||||
- **(a)** `cmd/erasure-server-pool.go:2282` 改为 `if !meta.Created.IsZero() { bucketInfo.Created = meta.Created }`。一行;恢复计划中"物理桶 Created 补齐路径"的本意;顺带修掉"无 metadata.bin 的桶在 `mc ls` 里创建时间为零"这一既有瑕疵。需复核 `bucketExists`、`hasBucket = !bi.CreatedAt.IsZero()` 等消费点 —— Created 由零变非零对它们都是变好。
|
||||
- **(b)** 另外在 `syncToAllPeers`(`cmd/site-replication.go:2138-2156`)把已在手的 `bucketInfo.Created` 兜底进 `meta.Created`,或在"有内容但 send==false"时打一条 indeterminate,避免静默漏发。
|
||||
|
||||
应补验证:
|
||||
- 把 `TestPeerBucketMetadataUnknownCreated` 的 `"physical-created"` 子用例**去掉 stub**:用真实 ObjectLayer,把 `newBucketMetadata(bucket)`(Created 为零)存盘并让缓存持有它,断言 `globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, tagXML)` 成功且 `Created` 被补齐为物理时间。**当前实现会失败。**
|
||||
- 一个 `syncToAllPeers` 用例:`Created == 0` 且 Tags 非空的桶,断言初次同步仍发 Tags 事件(或至少有诊断)。
|
||||
|
||||
---
|
||||
|
||||
### F2 — Policy 规范编码对 `Statement` 数组排序,与既有公开统计的顺序敏感 `Equals` 冲突:永久假 mismatch,heal 永不修复
|
||||
|
||||
**严重度 S2(公开状态/可观测性,非数据损失)· 已证实(源码级),需运行确认**
|
||||
|
||||
位置:
|
||||
- `cmd/bucket-metadata-replication.go:110` —— `sort.Slice` 作用于**每个**数组,含顶层 `Statement`
|
||||
- `cmd/bucket-metadata-replication.go:124-161` `canonicalBucketPolicy`
|
||||
- `cmd/site-replication.go:3796` `isBktPolicyReplicated` → `prev.Equals(*p)`(`silo-pkg .../policy/bucket-policy.go:190-194` 按 `Statements[i]` **下标**逐一比较,顺序敏感)
|
||||
- `cmd/site-replication-metadata.go:174` vs `:179` —— 本地 heal 写**源站原始字节**,远端 heal 经 `PeerBucketPolicyHandler` → `canonicalBucketPolicy` 写**规范字节**
|
||||
|
||||
最短反例:
|
||||
1. 站点 A 存在升级前写入的桶策略,statement 顺序不是规范字节序(极常见,例如 Deny 在 Allow 之前)。
|
||||
2. 加入新站点 B:`syncToAllPeers` → `initialBucketConfigReplicationEvent` 发送 **A 的原始字节**,`UpdatedAt = A.PolicyConfigUpdatedAt`。
|
||||
3. B 的 admin 入口解析后调 `PeerBucketPolicyHandler` → `canonicalBucketPolicy` → 存**排序后**字节。
|
||||
4. 此后 A/B 的比较键(canonical)完全相同、时间相同 → `compareBucketConfigStates == 0` → heal 永不写、永不发 RPC;而 `Equals` 因 statement 顺序不同返回 false → `mc admin replicate status` **永久**报 bucket policy mismatch,`ReplicatedBucketPolicies` 少计。
|
||||
|
||||
同机制第二条路径:同一轮 heal 中本地目标拿原始字节、远端目标拿规范字节,三站点集群会出现持久分叉。
|
||||
|
||||
为何是新问题:基线 `PeerBucketPolicyHandler` 用 `json.Marshal(policy)`,集合(Action/Resource/Principal)顺序随机但 **statement 切片顺序被 marshal/unmarshal 保持**,`Equals` 一直成立。
|
||||
|
||||
最小修复(三选一,推荐 a 或 b):
|
||||
- (a) `canonicalBucketPolicyJSON` 不对顶层 `Statement` 数组排序(只排集合数组)——顺序在全链路被保留,两站点独立解析同一文档仍得相同键。
|
||||
- (b) `isBktPolicyReplicated` 改用 `canonicalBucketPolicy` 的键比较,而不是 `Equals`。
|
||||
- (c) `healBucketConfig` 本地分支改写 `incoming.data`(与远端同一编码)——只修本地/远端分叉,**不**修 A 与新站点的分叉。
|
||||
|
||||
应补验证:双站点用例——A 侧直接把 legacy 顺序的策略字节写盘(绕过 canonical 编码器),join B,跑两轮 heal,断言 `SiteReplicationStatus` 不报 policy mismatch。
|
||||
|
||||
---
|
||||
|
||||
### F3 — heal 诊断以 ERROR 级、按"桶 × 字段"输出;正常瞬态也报警,且单一 key 会吞掉真实 RPC 失败
|
||||
|
||||
**严重度 S3 · 已证实**
|
||||
|
||||
位置:`cmd/site-replication-metadata.go:134-145`(两个诊断循环)、`:182-186`(peer 错误)、`cmd/logging.go:19-21`(`replLogOnceIf` 无 errKind ⇒ **ErrorKind**)
|
||||
|
||||
1. 第二个循环对 `!state.valid` 的目标打 `indeterminate / unusable peer`。`valid` 要求对端 `CreatedAt != 0` —— 而**对端还没有这个桶**时 `CreatedAt` 就是 0(`cmd/site-replication.go:3096-3103` 给缺桶站点填零值 `SRBucketInfo`)。于是"桶刚建、尚未传播"这种完全正常的瞬态,对每个桶产生 6 条 **ERROR**。
|
||||
2. 第一个循环在对端 `RemoteTargetConnectionErr`(`cmd/site-replication.go:3020-3026` 填空 ID)时,对**每个本地桶 × 6 字段**打 `missing peer`。一个站点掉线 ≈ 每小时 6×桶数 条 ERROR。
|
||||
3. 四种完全不同的情况(缺 peer / peer 不可用 / peer RPC 失败 / Created 未知)共用同一个 key `"bucket-metadata/<bucket>/<file>/indeterminate"` **且错误正文相同**,而 `logOnceIf` 只按 key+正文去重(`internal/logger/logonce.go:100-119`)。结果:**真正的 heal RPC 失败可能被同桶同字段的"缺 peer"消息顶掉而完全不打印**;基线版本这些失败走 `replLogIf`,必然打印。
|
||||
|
||||
最小修复:`target.CreatedAt.IsZero()`(桶不在对端)与 `found == false` 时不打诊断;把 4 种情况拆成不同 reason(`unreachable`/`peer-error`/`unknown-created`)避免 key 撞车;给 `logBucketConfigReplication` 传 `logger.WarningKind`。
|
||||
|
||||
---
|
||||
|
||||
### F4 — 三处用户可见语义变更未写入文档
|
||||
|
||||
**严重度 S3(文档/兼容)· 已证实(逐条比对 `docs/site-replication/README.md:65-118`)**
|
||||
|
||||
1. **空 Bucket Policy PUT 现在立即按删除处理**:`cmd/bucket-policy-handlers.go:102` + `cmd/bucket-metadata-replication.go:176-180` → `canonicalBucketPolicy` 对 `IsEmpty()` 返回 nil → 落盘 nil → `GetBucketPolicy` 从"200 + 空策略文档"变为 **404 NotFound**。计划 §1 明确写了"需要写入兼容说明",文档里没有。
|
||||
2. **零 Quota (`{}`) 在对端从"被删除"变为"保留 live 文档"**:`cmd/admin-bucket-handlers.go:79-96` 删除了出站 `bucketMeta.Quota = nil` 改写。这是正确的对齐(本地原本就保留 `{}`),但对端 `GetBucketQuotaConfig` 的结果会变。
|
||||
3. **`GET ?policy` 与 `mc admin bucket export` 的 JSON 形态改变**:`cmd/bucket-policy-handlers.go:202`、`cmd/admin-bucket-handlers.go:439` 改用 `canonicalBucketPolicy` → 对象键按字母序、集合数组与 **Statement 数组被排序**。语义等价(S3 策略求值与 statement 顺序无关),但字节级比对输出的工具会看到变化。
|
||||
|
||||
---
|
||||
|
||||
### F5 — 最小性:Policy 规范编码器接入 PUT / GET / export / peer 落盘,并非 #77 不变量所必需
|
||||
|
||||
**非缺陷,最小性判断 · 已证实**
|
||||
|
||||
比较键在 `bucketConfigPayload`(`cmd/bucket-metadata-replication.go:172-180`)里**从已解析策略现算**,与落盘字节无关。即使 PUT/peer 仍用 `json.Marshal`(字节不确定),两站点的键依旧相同,收敛性完全不受影响。
|
||||
|
||||
因此这部分改动带来的是两件**额外**的事:(a) 让原本被 `ActionSet.MarshalJSON`(`silo-pkg .../policy/actionset.go:144-148`,空集合报错)挡掉的 `NotAction`/`NotResource` 策略首次可以写入 —— 这是**功能新增**;(b) 直接导致 F2。
|
||||
|
||||
判断:如果作者**有意**支持负集合策略,应作为独立特性声明并单独记录/测试(目前只有 `site-replication-metadata_test.go:530-541` 一个用例);如果只为 #77,最小做法是 `canonicalBucketPolicy` 只用于比较键。我**不**主张必须删除——它确实修掉了"能存不能读"的潜在坑——但必须承认这是范围外的行为扩张,且未在文档中声明。
|
||||
|
||||
其它可删复杂度(都很小,不影响不变量):
|
||||
- `cmd/bucket-metadata-sys.go:151` 在零值结构体上用 `replicatedBucketConfig(&result.meta, configFile)` 做"是不是这六类"的判断,语义晦涩;一个 `isReplicatedBucketConfig(file) bool` 更清楚。
|
||||
- 修掉 F3 后,`healBucketConfig` 的第二个诊断循环(`:139-145`)可并入主循环。
|
||||
|
||||
---
|
||||
|
||||
### F6 — Nit
|
||||
|
||||
- `isBucketMetadataEqual`(`cmd/site-replication.go:5164`)现在只被测试引用,生产死代码。
|
||||
- `cmd/bucket-metadata-replication.go:36-38` 注释"Object Lock is applied before Versioning"对 bulk apply 循环成立,但 `healBuckets`(`cmd/site-replication.go:4745-4746`)先 heal Versioning 再 heal Object Lock。我推演过仍收敛(最多 2 个周期、无写入环),但注释与 heal 顺序不一致,建议补一句。
|
||||
|
||||
---
|
||||
|
||||
### F7 — 范围说明(非缺陷,但必须进入决策)
|
||||
|
||||
**默认 `MINIO_SITE_REPLICATION_METADATA_TOMBSTONES=off` 时,Tags / SSE / Quota 的"漏发删除"不会通过 heal 收敛。** 只有 Policy 墓碑默认导出(`cmd/site-replication.go:3953-3954` 无条件导出;另外三类在 `:3960 / :3980 / :3988` 被 gate 挡住)。
|
||||
|
||||
我完整推演了 off 模式,结论与文档一致:本地真实墓碑在锁内比较时**不会**被旧 PUT 复活(`applyBucketConfig` 用真实落盘状态比较);但持有旧数据的对端会每 30 秒发一次过期 RPC 被拒绝,**状态永不收敛**,直到运维在权威站点重新提交删除,或全站点升级后统一开启开关。
|
||||
|
||||
也就是说:**默认配置下交付的是"顺序正确 + 不复活 + Policy 删除可 heal",不是"四类删除都能自愈"。** 这是 v4 计划的既定取舍(计划 §提交 3 表格),实现与文档都如实写了;我在此只是确保批准时看到这一点。
|
||||
|
||||
---
|
||||
|
||||
### F8 — 证据不足项
|
||||
|
||||
1. **F2 对双站点实验不可见**:`twosite/main.go:141-144` 的所有策略都经 `SRPeerReplicateBucketMeta` → `canonicalBucketPolicy` 写入,两侧都是规范字节,`states()`(`:160-164`)的字节比较自然通过。**从未构造过"升级前旧编码字节"的策略。**
|
||||
2. **F1 被 stub 掩盖**:`cmd/site-replication-metadata-gate_test.go:151-159` 自定义 `GetBucketInfo`,在 `opts.NoMetadata` 时直接返回物理 Created,绕过了 `erasureServerPools.GetBucketInfo` 的缓存覆盖。`"physical-created"` 子用例证明的是一个**生产中不会发生**的行为。
|
||||
3. **"稳态 0 metadata RPC"的边界**:`twosite/main.go:182-190` 只统计 `event.Bucket == bucket` 的单桶、双站点、**gate=on**。结论有效但窄;gate=off 的稳态不为零(文档已声明),多桶/多站点未观测。
|
||||
4. **二进制身份**:`manifest.json` 给了 `silo-final` 的 sha256,但目录里没有 `--version`/build-info 输出(驱动用 `--quiet --json` 启动),`runtime-final.log` 也没有版本行。验收自述"编译信息为 `c8f264f79 + dirty`"是**诚实的**,且日志里的 gate 行为(`gate=on/off`、`off exporter exposed new tombstone`)只能来自第三个提交的生产代码,所以**没有夸大**;但也**不能从证据目录内独立复核**。补一份 `silo-final --version` 或 `go build` 复现即可闭环。
|
||||
5. **baseline.log 行号与最终测试文件不一致**(日志 93/105/117 vs 现文件 96/97/109/121):说明"修复前复现"跑的是测试文件的早期版本。可接受,但严格讲不是同一份用例。
|
||||
|
||||
---
|
||||
|
||||
### F9 — 残留边界:桶世代冲突(计划内已声明,非本轮回归)
|
||||
|
||||
各站点 `Created` 不同时,A 的 baseline-live(`at == A.Created`)在 B 上会被重算为 `real`(`newBucketConfigState` 用**目标**的 `created` 判定,`cmd/bucket-metadata-replication.go:215-219`),可能压过 B 的真实墓碑。计划明确排除在收敛承诺外,且 `AddPeerClusters`(`cmd/site-replication.go:458-466`,"only one cluster may have data")封死了最常见入口。剩余入口:分区期间两站点各自建同名桶,或 #78 接管。基线版本在同场景下是**不确定**的(seed 首个 map 项),所以不算回归。
|
||||
|
||||
---
|
||||
|
||||
### F10 — 覆盖缺口:接管时 `Created` 前移越过真实修改时间
|
||||
|
||||
`rebaseBucketConfigDefaults`(`cmd/bucket-metadata-replication.go:306-318`)只调整"零/等于旧 Created"的默认时间。若 `opts.CreatedAt` 晚于某个**真实**字段时间,该字段变成 `at < Created` ⇒ `valid == false`:作为 heal 源 `candidate()` 为 false;作为 heal 目标 `incoming.valid > current.valid` ⇒ **必被覆盖**,本地真实配置被丢弃。
|
||||
|
||||
在"远端世代胜出"的语义下可以论证这是对的,但 `TestPeerBucketAdoptionRebasesOnlyDefaults`(`cmd/site-replication-metadata_test.go:394-425`)用的 shift 是 ±1h 而真实时间在 `created+2h`,**恰好没有覆盖这一情形**。建议加一个 `shift = +3h` 的子用例,把期望行为固定下来。
|
||||
|
||||
---
|
||||
|
||||
## 2. 六类配置 × 各入口 覆盖判断
|
||||
|
||||
| 配置 | 本地写 | typed peer | bulk | import | initial sync | heal | 接管 | 结论 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| Policy | ✅ `bucket-policy-handlers.go:108/152`,hook 用 `result.meta`+`result.updatedAt` | ✅ `site-replication.go:1716-1730` | ✅ `len(item.Policy)!=0` 判"已提供" | ✅ 共同 `commitAt` + 空策略另发专用 nil 事件(`admin-bucket-handlers.go:1150-1155`) | ✅ gate 控墓碑 | ✅ | ✅ rebase | 通过(F2/F4 为附带) |
|
||||
| Tags | ✅ `bucket-handlers.go:1940/2021` | ✅ `:1733-1747` | ✅ `*string != nil` | ✅ | ✅ gate | ✅ 补齐 `UpdatedAt`(旧版 heal 缺此字段) | ✅ | 通过 |
|
||||
| SSE | ✅ `bucket-encryption-handlers.go:106/199` | ✅ `:1787-1801` | ✅ | ✅ | ✅ gate | ✅ | ✅ | 通过 |
|
||||
| Quota | ✅ `admin-bucket-handlers.go:85-99`(删掉零值改写) | ✅ `:2032-2047` | ✅ `len(item.Quota)!=0` | ✅ | ✅ gate | ✅ 含缓存清除(`parse=false` 修复) | ✅ | 通过 |
|
||||
| Versioning | ✅ `bucket-versioning-handler.go:100-116`,广播用归一后的 `result.meta` | ✅ 空=no-op | ✅ 空=no-op | ✅ 归一后落盘并广播 | ⛔ 不发(由 MakeBucketHook bootstrap + heal 对齐,与基线一致) | ✅ 按**目标** Lock 状态归一后比较 | ✅ `enablePeerBucketVersioning` 用 `localBucketConfigUpdatedAt` | 通过 |
|
||||
| Object Lock | ✅ `bucket-handlers.go:1841-1852` | ✅ 空=no-op,保留 `item.Tags` legacy 回退 | ✅ 空=no-op | ✅ | ✅ | ✅ | ✅ | 通过 |
|
||||
|
||||
逐条核对结论:
|
||||
|
||||
- **来源时间不会变成本地 now**:六个 typed handler 全部删除了锁外 `GetXConfig()` 判旧,统一走 `updateAndParseMetadata(..., &updatedAt)`;非零 `sourceTime` 直接 `sourceTime.UTC()`(`bucket-metadata-sys.go:191-193`)。只有 `sourceTime == nil`(本地写)或为零(legacy 兼容)才分配本地单调时间。✔
|
||||
- **本地写严格单调**:`localBucketConfigUpdatedAt`(`:255-263`)保证 `> Created` 且 `> 当前字段时间`,涵盖"已有未来时间"。✔
|
||||
- **重复/乱序不保存不广播**:`applyBucketConfig` 在 `compare <= 0` 返回 `changed=false`,据此跳过 `saveMetadata` 与 `LoadBucketMetadata`;bulk 用 `changed` 汇总后一次保存。✔
|
||||
我特别核对了所有使用 `result.meta`/`result.updatedAt` 的本地 handler:在这些路径上 `changed` 恒为 true(`localBucketConfigUpdatedAt` 使 `incoming.at` 严格更大,且 `incoming.candidate()` 恒真),所以**不会**出现"零时间 + 空载荷"被当成删除广播出去。这是一个隐式依赖,建议加一行注释或断言固定住。
|
||||
- **整桶 `.metadata.bin` 读-比较-写全在既有分布式锁内**:`updateAndParseMetadata`、`PeerBucketMetadataUpdateHandler`、import 最终提交、`PeerBucketMakeWithVersioningHandler`、CORS 路径,我逐个确认锁的获取在读之前、释放在 `saveMetadata` 之后、fan-out 在释放之后。✔
|
||||
- **无关字段 / CORS / lifecycle 不被覆盖**:所有写路径都是锁内**重新加载**后只改目标字段;import 用 `applyImportedBucketMetadata` 只按 `fields` 拷贝并 `bytes.Clone`。✔
|
||||
- **缓存快照正确**:`saveMetadata` 改收 `*BucketMetadata`,`meta.Save` 内部 `parseAllConfigs` 回写归一化结果,`sys.Set(name, *meta)` 发布的是提交后快照;调用方拿到的 `result.meta` 是同一份值拷贝,不会原地修改已发布引用。✔
|
||||
- **`parse` 被强制为 false 对缓存无害**:`Save()` 先 `parseAllConfigs`,发布到 `metadataMap` 的解析字段是新的;而"删除后 quota 解析残留"正是靠 `parse=false` 的新加载对象修掉的(`parseAllConfigs` 对空 `QuotaConfigJSON` **不会**把 `quotaConfig` 置 nil,`bucket-metadata.go:400-405`)。✔
|
||||
- **锁/返回值/错误处理**:`unlock()` + `locked=false` 模式保留;`updateAndParse`、`Update`、`Delete` 对外签名不变。✔
|
||||
|
||||
---
|
||||
|
||||
## 3. 六字段 state 比较:传递性、交换序无关、幂等收敛
|
||||
|
||||
`compareBucketConfigStates`(`bucket-metadata-replication.go:227-253`)实际是按 `(valid, real, real?at:—, real?isTombstone:—, key)` 的字典序全序。
|
||||
|
||||
- **传递 / 反对称**:是。`real == false` 时不比较 `at`(只比 key),但空 baseline 的 key 为 nil,`bytes.Compare` 使其恒最小,效果等同"空 baseline 永不获胜"。✔
|
||||
- **交换顺序无关**:`latestBucketConfig` 取全序最大值,与 map 遍历顺序无关;`applyBucketConfig` 同样只依赖全序。`TestLatestBucketConfigCandidates` 跑了全部 6 种排列。✔
|
||||
- **幂等收敛**:写入后目标状态等于源状态 ⇒ 下一轮 `compare == 0` ⇒ 不写不发。我另外手工推演了 Object Lock/Versioning 的"Enabled vs Suspended 同时间"场景,归一后双侧都不再写,稳定无振荡。✔
|
||||
- **baseline Created 与真修改的区分**:`real = valid && at.After(created)`,零字段时间先回填为 `created`(与 `defaultTimestamps()` 一致)。✔
|
||||
- **同时间删除胜出**:`:244-250`,仅在 `a.real` 分支内生效,空 baseline 不会借此删配置。✔
|
||||
- **等时不同载荷的稳定键**:
|
||||
- **Policy**:递归排序的规范 JSON + `json.RawMessage` 保留整数精度(测试 `:308-311` 验证 `9007199254740993` 不失真)。我对照了 `silo-pkg .../policy/bucket-policy-statement.go:27-36`,`BPStatement` 的 8 个字段(SID/Effect/Principal/Actions/NotActions/Resources/NotResources/Conditions)**全部覆盖,无字段静默丢失**;`ParseBucketPolicyConfig` 先做 `Validate` 保证 `Principal.MarshalJSON` 不会失败。✔ 但顶层 Statement 排序引出 F2。
|
||||
- **Quota**:`json.Marshal(parseBucketQuota(...))`。零 quota 仍是 live 文档(`candidate()` 靠 `real`,不靠 `len(data)`)。`{}` 与对端重编码字节不同但键相同,不产生 heal 环;`isBktQuotaCfgReplicated` 按解析值比较,不会误报。✔
|
||||
- **Tags / SSE / Object Lock**:有效文档字节,保留大小写与实际内容。✔
|
||||
- **Versioning**:先按**该站点**的 Object Lock 归一(`effectiveBucketVersioning`),键即归一后的文档,避免"保存阶段隐式改写"造成的空转。✔
|
||||
- **空策略**:`bucketConfigPayload` 对 `cfg.IsEmpty()` 返回 `(nil, nil, nil)`,与 peer 侧既有"空策略=删除"解释统一。✔(副作用见 F4-1)
|
||||
- **Object Lock 强制 Versioning 后的持久化 vs 广播**:本地只广播 Object Lock 事件,派生的 Versioning 由对端在 `Save → parseAllConfigs` 自行推导,**内容一致、时间各自保留**;heal 用归一后的有效文档比较,最多一次写入即收敛。与计划一致。✔
|
||||
|
||||
---
|
||||
|
||||
## 4. Created / 时间的异常输入
|
||||
|
||||
| 输入 | 行为 | 判断 |
|
||||
|---|---|---|
|
||||
| Created 为零 / metadata.bin 缺失 | 六类写入报错;初次同步静默跳过 | **F1,回归** |
|
||||
| 字段时间为零 | 回填为 Created ⇒ baseline | ✔ 与 `defaultTimestamps()` 一致 |
|
||||
| 事件时间 == Created | 可更新仍为 baseline 的字段,时间保存为 Created;同时间 nil/空只算空 baseline,不删除 | ✔ 有用例 |
|
||||
| 事件时间 < Created | `before-created`,跳过 + 单条日志;heal 对该目标 `continue` | ✔ |
|
||||
| 未来时间 | 接受为真实状态;后续本地写用 `max(now, at+1ns)` 压过 | ✔ 有用例 |
|
||||
| 各站点 Created 不同 | 见 F9(计划外,已声明) | 残留 |
|
||||
| 空 deployment ID | `latestBucketConfig` / `healBucketConfig` 均 `id == "" \|\| !known` 跳过 | ✔ 有用例 |
|
||||
| peer 报错 / 缺元数据 | `candidate()` 为 false ⇒ 不参与选源;作为目标 `CreatedAt.IsZero()` ⇒ `continue`。**不会被当成删除** | ✔ |
|
||||
| 单个 peer 不可达 | 记录后继续其它目标(`site-replication-metadata.go:182-186`),不因 map 顺序放弃健康站点 | ✔ 有用例(`"broken"`) |
|
||||
|
||||
**"已知历史不可恢复"与"新实现回归"的区分**:
|
||||
- 历史不可恢复(可接受):旧版到达时间污染、legacy-zero 事件产生的新本地时间、桶世代分歧。
|
||||
- 新实现回归(应修):F1 的零 Created 硬失败与初次同步静默漏发;F2 的永久假 mismatch;F3 的 ERROR 噪声与日志互相顶替。
|
||||
|
||||
---
|
||||
|
||||
## 5. 兼容路径与开关
|
||||
|
||||
- **typed 零时间兼容**:`sourceTime != nil && IsZero()` ⇒ 分配本地时间 + 一条 `legacy-zero`(`bucket-metadata-sys.go:186-190`)。不受 gate 影响,不在源时间排序保证内。✔
|
||||
- **bulk 零时间拒绝**:`PeerBucketMetadataUpdateHandler:1610-1612` ⇒ `errInvalidArgument`,与现状一致;import hook 恒带非零 `commitAt`。✔
|
||||
- **省略 vs 显式 null / 空串**:`madmin.SRBucketMeta` 全字段 `omitempty`(`madmin-go v3.0.110 cluster-commands.go:501-538`),因此
|
||||
- `json.RawMessage`(Policy/Quota):省略 ⇒ nil;显式 `null` ⇒ `[]byte("null")`,走 `len(...)!=0` 判为"已提供",再交既有解析器(Policy `null` ⇒ 语义空 ⇒ 删除;Quota `null` ⇒ 零值文档 ⇒ live)。✔ 与计划 §1 完全一致。
|
||||
- `*string`:省略/`null` ⇒ nil ⇒ 不动;空串 ⇒ 已提供且内容为空 ⇒ 四类删除、两类 no-op。✔
|
||||
- 这些由 `TestPeerBucketMetadataWireAtomicity` 通过**真实 admin 路由 + 真实 JSON 编解码**覆盖(`applySRBucketMetaViaAdmin` 走 `registerAdminRouter`),不是结构体直传。这是这批测试里质量最高的一块。
|
||||
- **direct hook 与 heal 的删除传播**:普通 DELETE 事件在 gate off 下照常复制;heal 依赖导出可见性 ⇒ 见 F7。
|
||||
- **初次同步**:保留原五类范围(不含 Versioning),gate 控制是否发送真实墓碑。✔
|
||||
- **gate 覆盖点是否遗漏**:我检查了所有导出/初次发送点 —— `SiteReplicationMetaInfo` 的 Tags/Quota/SSE 三处(`:3960/:3980/:3988`)+ `initialBucketConfigReplicationEvent:51`。Policy 墓碑**故意**不受 gate 控制(原本已导出);Versioning/Object Lock 无墓碑概念。**未发现遗漏的开关覆盖点。** ✔
|
||||
- **滚动升级/降级**:off 时新增墓碑信息不导出,旧节点不会收到它无法正确处理的 Quota heal 墓碑(文档点名了"旧版会留下已解析 quota 残留"这一实证依据)。✔ 但 off **不等于**与旧版同构:修复端的**接收**行为已经变了(锁内排序、空 baseline 不删除、重复不写),这对旧版发来的事件是更安全的方向,混版冒烟也验证了普通 PUT/DELETE 互通。降级路径文档要求先关开关再滚降;唯一可补的一句是"墓碑时间字段本来就在 schema 内,旧版只是不导出,降级不会产生解析错误"。
|
||||
|
||||
---
|
||||
|
||||
## 6. 三个独立判断
|
||||
|
||||
**最小?—— 基本是,有一处可争议的扩张。**
|
||||
生产 Go 代码净增约 37 行,六份重复的 heal / peer apply 被一套 helper 替代;没有新 schema、没有新 wire 字段、没有新锁、没有能力协商、没有迁移系统。唯一超出必要的是 **Policy 规范编码器接入写/读/导出路径**(F5)——它不是收敛所必需的,并且直接导致 F2。其余(`bucketMetadataUpdate` 提交快照、`ensureBucketMetadataCreated`、`rebaseBucketConfigDefaults`、gate)我都能各自对应到一个已证实的缺陷或本次修复直接触及的路径,**没有**可以无损删除的部分。
|
||||
|
||||
**充分?—— 对"已声明的范围"是;对"缺陷标题"不是。**
|
||||
在同一桶世代、Created 已知、gate=on 的前提下,六类配置的源时间、锁范围、删除状态参与 heal、等时冲突裁决、重复/乱序抑制都成立。但两个口子要明说:(1) 默认 gate=off ⇒ Tags/SSE/Quota 的漏发删除不收敛(F7,计划内取舍);(2) Created 未知的桶从"能写"变成"不能写"(F1,计划外回归)。
|
||||
|
||||
**必要?—— 是。**
|
||||
每一项改动都能对应到 `baseline.log` 里的实测失败(SOURCE_TIME / NEWER_DELETE / STALE_RESURRECTION / BULK_STALE_OVERWRITE / CHECK_OUTSIDE_LOCK)或计划中论证过的路径。`rebaseBucketConfigDefaults` 这种看起来最"多余"的小分支,实际是 `Created` 一旦参与 baseline/tombstone 判定后的必然补丁(不加则接管时默认值会变成假墓碑)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 新增设计记录应准确保留的内容
|
||||
|
||||
**关键决策**
|
||||
1. baseline / live / tombstone 三态由 `(Created, 字段时间, 载荷空否)` 推导,**不新增 schema 字段**;`at == Created` 定义为 baseline。代价:必须知道 Created(⇒ F1 的根因)。
|
||||
2. 全序:`valid > real > at > 同时间墓碑胜 > 稳定内容键`。deployment ID **不**参与比较,也不落盘。
|
||||
3. 比较键是"已解析配置的纯函数":Policy 走递归排序的规范 JSON,Quota 走解析后 `json.Marshal`,XML 走有效文档字节,Versioning 先按本站点 Object Lock 归一。
|
||||
4. Versioning / Object Lock 为 update-only:空事件恒为 no-op,空值不是候选。
|
||||
5. 读-比较-写整体在**既有** `metadata.lock` 内;`saveMetadata` 收指针以便调用方拿到提交后快照,保证"落盘状态 / 出站事件 / 源时间"三元组一致。
|
||||
6. 新增删除信息的**导出**用启动开关 gate,默认 off;开关不探测远端能力,启用条件是全站点全节点已升级且旧请求排空。
|
||||
7. 专用 peer 事件的零时间保留兼容例外(分配本地时间 + 限频 `legacy-zero`);bulk 零时间仍按现状拒绝。
|
||||
|
||||
**被拒绝的方案**(记录以免复议)
|
||||
- 新增 HLC / 向量时钟 / 新 wire 字段 / 能力协商 / 通用复制框架 / 新锁或重试系统。
|
||||
- 用 `len(payload)==0 ⇒ 删除` 统一处理 bulk(会把"省略"误判为删除)。
|
||||
- 把 `BucketPolicy.Equals`(忽略 Sid、对 Statement 顺序敏感)叠加为第二套判等规则。
|
||||
- 把混版长期测试作为提交门槛。
|
||||
- 把未修改上游 MinIO 的兼容性当作必需门禁(AGENTS.md:正式支持 PGSTY 栈,上游兼容为尽力而为)。
|
||||
|
||||
**限制**
|
||||
- gate=off 期间 Tags/SSE/Quota 墓碑不可见,不承诺删除收敛,且存在被拒绝的周期性 RPC。
|
||||
- 旧版到达时间污染、legacy-zero 产生的新本地时间、桶创建世代分歧**无法自动反推**,需运维在权威站点重新提交。
|
||||
- `Created` 未知的桶不在收敛承诺内(并且按当前实现直接不可写,见 F1)。
|
||||
- "比较键相同但落盘字节不同"(Quota 原始 JSON、Policy 旧编码)是允许的稳定状态,公开 mismatch 统计必须与比较键口径一致(见 F2)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 建议处置顺序
|
||||
|
||||
1. 判定 F1 的条件(支持矩阵里有没有 `Created == 0` / 无 `.metadata.bin` 的桶)。有 ⇒ 先修再合。
|
||||
2. 修 F2(推荐:不排序顶层 `Statement`,或让 `isBktPolicyReplicated` 改用规范键)。
|
||||
3. 修 F3(ERROR→Warning、缺桶不报、reason 拆分)。
|
||||
4. 补 F4 的三条文档。
|
||||
5. 补测试:F10 的接管用例、F8-1 的 legacy 策略字节双站点用例、F8-2 去 stub 的 Created 用例、F8-4 的二进制身份记录。
|
||||
6. F5 / F6 由作者判断,可留作后续。
|
||||
@@ -0,0 +1 @@
|
||||
ok github.com/minio/minio/cmd 6.864s
|
||||
@@ -0,0 +1 @@
|
||||
ok github.com/minio/minio/cmd 20.694s
|
||||
@@ -0,0 +1,57 @@
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery (0.57s)
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/ErasureSD/object-lock.xml/missing=false (0.00s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update object-lock.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/ErasureSD/versioning.xml/missing=false (0.00s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update versioning.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/ErasureSD/policy.json/missing=false (0.00s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update policy.json: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/ErasureSD/tagging.xml/missing=false (0.00s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update tagging.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/ErasureSD/bucket-encryption.xml/missing=false (0.00s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update bucket-encryption.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/ErasureSD/quota.json/missing=false (0.00s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update quota.json: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/ErasureSD/object-lock.xml/missing=true (0.00s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update object-lock.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/ErasureSD/versioning.xml/missing=true (0.00s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update versioning.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/ErasureSD/policy.json/missing=true (0.00s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update policy.json: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/ErasureSD/tagging.xml/missing=true (0.00s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update tagging.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/ErasureSD/bucket-encryption.xml/missing=true (0.00s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update bucket-encryption.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/ErasureSD/quota.json/missing=true (0.00s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update quota.json: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/Erasure/object-lock.xml/missing=false (0.02s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update object-lock.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/Erasure/versioning.xml/missing=false (0.02s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update versioning.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/Erasure/policy.json/missing=false (0.02s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update policy.json: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/Erasure/tagging.xml/missing=false (0.02s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update tagging.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/Erasure/bucket-encryption.xml/missing=false (0.06s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update bucket-encryption.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/Erasure/quota.json/missing=false (0.02s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update quota.json: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/Erasure/object-lock.xml/missing=true (0.04s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update object-lock.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/Erasure/versioning.xml/missing=true (0.02s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update versioning.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/Erasure/policy.json/missing=true (0.02s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update policy.json: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/Erasure/tagging.xml/missing=true (0.01s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update tagging.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/Erasure/bucket-encryption.xml/missing=true (0.02s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update bucket-encryption.xml: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketMetadataPhysicalCreatedRecovery/Erasure/quota.json/missing=true (0.02s)
|
||||
site-replication-metadata-gate_test.go:227: bucket without a recorded creation time cannot update quota.json: bucket metadata creation time is unknown
|
||||
--- FAIL: TestBucketPolicyReplicationStatusLegacyOrder (0.31s)
|
||||
--- FAIL: TestBucketPolicyReplicationStatusLegacyOrder/ErasureSD (0.01s)
|
||||
site-replication-metadata_test.go:348: equivalent legacy and received policy reported as permanently mismatched
|
||||
--- FAIL: TestBucketPolicyReplicationStatusLegacyOrder/Erasure (0.06s)
|
||||
site-replication-metadata_test.go:348: equivalent legacy and received policy reported as permanently mismatched
|
||||
FAIL
|
||||
FAIL github.com/minio/minio/cmd 2.940s
|
||||
FAIL
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
--- FAIL: TestHealBucketConfigDiagnostics (0.29s)
|
||||
--- FAIL: TestHealBucketConfigDiagnostics/ErasureSD (0.00s)
|
||||
site-replication-metadata-heal_test.go:80: empty baselines produced diagnostics
|
||||
--- FAIL: TestHealBucketConfigDiagnostics/Erasure (0.01s)
|
||||
site-replication-metadata-heal_test.go:80: empty baselines produced diagnostics
|
||||
FAIL
|
||||
FAIL github.com/minio/minio/cmd 2.721s
|
||||
FAIL
|
||||
@@ -0,0 +1,8 @@
|
||||
--- FAIL: TestBucketMetadataInitialSyncPhysicalCreated (0.47s)
|
||||
--- FAIL: TestBucketMetadataInitialSyncPhysicalCreated/ErasureSD (0.04s)
|
||||
site-replication-metadata-gate_test.go:289: peer creation time "0001-01-01T00:00:00Z", want physical time 2026-09-12 06:03:09 +0000 UTC
|
||||
--- FAIL: TestBucketMetadataInitialSyncPhysicalCreated/Erasure (0.08s)
|
||||
site-replication-metadata-gate_test.go:289: peer creation time "0001-01-01T00:00:00Z", want physical time 2026-09-12 06:03:09 +0000 UTC
|
||||
FAIL
|
||||
FAIL github.com/minio/minio/cmd 2.576s
|
||||
FAIL
|
||||
@@ -0,0 +1,18 @@
|
||||
--- FAIL: TestHealBucketConfigWithoutSourceDiagnostics (0.36s)
|
||||
--- FAIL: TestHealBucketConfigWithoutSourceDiagnostics/ErasureSD (0.00s)
|
||||
--- FAIL: TestHealBucketConfigWithoutSourceDiagnostics/ErasureSD/unknown-created (0.00s)
|
||||
site-replication-metadata-heal_test.go:155: got 0 diagnostics, want 1: []
|
||||
--- FAIL: TestHealBucketConfigWithoutSourceDiagnostics/ErasureSD/malformed (0.00s)
|
||||
site-replication-metadata-heal_test.go:155: got 0 diagnostics, want 1: []
|
||||
--- FAIL: TestHealBucketConfigWithoutSourceDiagnostics/ErasureSD/before-created (0.00s)
|
||||
site-replication-metadata-heal_test.go:155: got 0 diagnostics, want 1: []
|
||||
--- FAIL: TestHealBucketConfigWithoutSourceDiagnostics/Erasure (0.01s)
|
||||
--- FAIL: TestHealBucketConfigWithoutSourceDiagnostics/Erasure/unknown-created (0.00s)
|
||||
site-replication-metadata-heal_test.go:155: got 0 diagnostics, want 1: []
|
||||
--- FAIL: TestHealBucketConfigWithoutSourceDiagnostics/Erasure/malformed (0.00s)
|
||||
site-replication-metadata-heal_test.go:155: got 0 diagnostics, want 1: []
|
||||
--- FAIL: TestHealBucketConfigWithoutSourceDiagnostics/Erasure/before-created (0.00s)
|
||||
site-replication-metadata-heal_test.go:155: got 0 diagnostics, want 1: []
|
||||
FAIL
|
||||
FAIL github.com/minio/minio/cmd 2.430s
|
||||
FAIL
|
||||
@@ -0,0 +1,174 @@
|
||||
# SILO #77 修正提交独立复审(固定 `62cf066ff529c7d281703daa365f555cebba717a`)
|
||||
|
||||
审查方式:只读。以 worktree 生产源码为准(不只读 diff),交叉核对 `silo-pkg v3.13.4-0.20260910091716` 依赖源码、证据目录与设计记录。上一轮报告只作为待验证的命题,不作为已确认结论。
|
||||
|
||||
---
|
||||
|
||||
## 0. 结论
|
||||
|
||||
**GO_WITH_NONBLOCKING_NOTES**
|
||||
|
||||
- **阻断项:0**(无条件 0,条件性 0)
|
||||
- 上一轮的条件性阻断 F1 与 S2 级 F2 均已在生产路径上真正关闭,并有"最终测试 + 旧生产代码覆盖"的复现证据(`round-2/confirmed-before.log`)。
|
||||
- 新增 8 项非阻断发现(NB-1 ~ NB-8),其中 3 项是行为残留(S3/S4),5 项是证据与文档表述准确性问题。
|
||||
- 一处需要纠正上一轮的**判断**(不是纠正实现):F5 中"Policy 规范编码器接入 GET/export/peer 属于超出必要的扩张"这一说法不成立——这三个调用点持有的是**已解析策略**,必须有编码器,而旧编码器对解析器已支持的负集合策略**必然报错**。真正"可选"的只有 PUT/import 两处,且因 `DisallowUnknownFields` 而无损。
|
||||
|
||||
---
|
||||
|
||||
## 1. F1–F10 关闭状态
|
||||
|
||||
| 项 | 上轮判定 | 本轮核实 | 状态 |
|
||||
|---|---|---|---|
|
||||
| F1 零 Created 桶六类配置写入硬失败、初次同步静默漏发 | S1 条件阻断 | 生产路径已修复并有真实 ObjectLayer + 旧码覆盖复现 | **关闭**(残留见 NB-1) |
|
||||
| F2 Policy 状态口径与收敛键冲突导致永久假 mismatch | S2 | `isBktPolicyReplicated` 改用同一 canonical 键,per-site presence 不变,无 SID/ID/Condition/未知值回归 | **关闭** |
|
||||
| F3 heal 诊断 ERROR 级、key 互相顶替、正常瞬态误报 | S3 | ERROR→Warning、四类原因各自持 key、缺桶不报 = 已关闭;"全无有效来源即完全静默"与"按桶×字段扇出"未处理 | **部分关闭**(NB-2、NB-3) |
|
||||
| F4 三处语义变更未文档化 | S3 | 上轮前两项系误读(原提交 README 已有,见 diff 中为 context 行);第三项已补入 `docs/site-replication/README.md:129-134` | **关闭 + 原误读已纠正** |
|
||||
| F5 Policy 规范编码器接入面 | 最小性争议 | 判断更新:GET/export/peer 非可选;PUT/import 可选但无损。建议保留现状 | **关闭(判断修正)** |
|
||||
| F6 死代码与过时注释 | nit | 注释已修(`bucket-metadata-replication.go:35-37`);`isBucketMetadataEqual` 仍为测试专用 | **关闭(保留 nit,见 NB-5)** |
|
||||
| F7 gate=off 不承诺四类删除自愈 | 范围说明 | README:75-89、设计记录 §rollout/§limits 均如实声明,未说成已自动修复 | **保持(如实)** |
|
||||
| F8 证据缺口(stub / 旧顺序策略 / 二进制身份) | 证据不足 | stub 已去除、旧顺序用例已补、二进制身份已闭环(`--version` 含 commit-id + 对照基线二进制) | **关闭**(但 NB-4:两份旧日志不可用作证据) |
|
||||
| F9 桶世代冲突 | 范围外 | 未被声称已修;设计记录 §limits-3 明确 | **保持(如实)** |
|
||||
| F10 接管 Created 越过真实字段时间 | 覆盖缺口 | 补 `shift=3h` 子用例,钉住"旧世代状态不得升级为有效来源" | **关闭(半边,见 NB-7)** |
|
||||
|
||||
---
|
||||
|
||||
## 2. 本轮重点逐项核实
|
||||
|
||||
### F1 —— 真正修好了,机制比上轮建议更完整
|
||||
|
||||
**`NoMetadata=true` 返回物理探测**(`cmd/erasure-server-pool.go:2279-2283`)。我枚举了全仓库 `NoMetadata` 的全部生产调用点,只有三处:
|
||||
|
||||
- `cmd/bucket-metadata-sys.go:272`(`saveMetadata` 存在性重查,只看 error)
|
||||
- `cmd/bucket-metadata.go:306`(迁移锁内存在性重查,只看 error)
|
||||
- `cmd/bucket-metadata-replication.go:281`(新增的 Created 恢复,正是要物理值)
|
||||
|
||||
其余全部是 `BucketOptions{}`(`NoMetadata=false`),行为不变。被跳过的 `globalBucketMetadataSys.Get()`(`bucket-metadata-sys.go:410-424`)是**纯缓存读、无副作用、不触发加载**,因此跳过它不会改变任何缓存预热或懒加载行为。`Versioning`/`ObjectLocking` 两个字段也只在非 NoMetadata 分支填,与 `ListBuckets`(`:2402/2422` 只在 `!NoMetadata` 时覆盖 Created)语义一致。**无调用者回归。**
|
||||
|
||||
**世代一致性链路完整**(这一点比上轮提出的方案更强,值得记录):
|
||||
`syncToAllPeers`(`site-replication.go:2145-2153`)恢复 `meta.Created` → 传入 `MakeBucketOptions.CreatedAt` → `MakeBucketHook`(`:821-826`)在缓存值为零时回落到 `opts.CreatedAt`(此时本地磁盘仍是 0,所以这个回落是必需的)→ **本地**分支 `PeerBucketMakeWithVersioningHandler`(`:943-966`)在 `metadata.lock` 内 `SetCreatedAt(opts.CreatedAt)` + `rebaseBucketConfigDefaults` + `saveMetadata`,**远端**分支通过 `optsMap["createdAt"]` 收到同一个值。也就是说本地并非"只在内存里补一下",而是与全部对端落盘同一个世代值。这条链路 diff 上看不出来,必须读 `MakeBucketHook` 的 `concDo` 第一个闭包才能确认。
|
||||
|
||||
四个恢复点的锁位置也确认无误:写路径(`bucket-metadata-sys.go:186`,锁内)、bulk(`site-replication.go:1674`,锁内)、import(`admin-bucket-handlers.go:1092`,锁内)、初次同步(锁外探测但不落盘,落盘由上面的钩子在锁内完成)。
|
||||
|
||||
**"无法恢复时显式错误"是否正确:正确。** 静默发明 `UTCNow()` 会造出一个不可撤销、且会向全站点传播的假世代;而显式失败保留原数据、可重试、并留一条有界 `indeterminate`。`syncToAllPeers` 因此让整个 `AddPeerClusters` 失败——这在真实文件系统上基本不可达(`StatVol` 用目录 `ModTime`,存在即非零),且 `AddPeerClusters` 是可重试的交互式管理操作,代价可接受。
|
||||
|
||||
**真实用例是否覆盖生产路径:是。** `TestBucketMetadataPhysicalCreatedRecovery`(`site-replication-metadata-gate_test.go:206-237`)通过 `globalBucketMetadataSys.save` 让**缓存持有 Created=0**,再走 `Update` → `updateAndParseMetadata` → `ensureBucketMetadataCreated` → 真实 `erasureServerPools.GetBucketInfo`;`ExecObjectLayerAPITest`(`test-utils_test.go:1766/1795`)两个后端都是真实 `erasureServerPools`,因此缓存覆盖这一段是真跑到的。`setPhysicalBucketCreated` 直接 `os.Chtimes` 每块盘的桶目录,正是 `xl-storage.go:1022-1028` 取值的来源。六类配置 × `missing=true/false` 全覆盖。
|
||||
|
||||
### F2 —— 关闭,且口径现在六类全部自洽
|
||||
|
||||
`site-replication.go:3782-3809`。状态侧的 `policies[i]` 来自 `:3436` 对**各站点原始字节的解析**,与 heal 的 `bucketConfigPayload`→`canonicalBucketPolicy`(`bucket-metadata-replication.go:191`)是同一个纯函数,因此 peer 重排 Statement 后的永久假 mismatch 被彻底关闭。`numPolicies != total` 的 per-site presence 计数原样保留(`:3783-3792`),并有断言覆盖。
|
||||
|
||||
回归面逐条查过:
|
||||
|
||||
- **SID**:旧 `BPStatement.Equals`(silo-pkg `bucket-policy-statement.go:148-171`)**不比较 SID**,新键包含 `Sid`。这是方向正确的收紧——heal 的收敛键同样含 SID,所以状态报出的 mismatch 是**真实且会被 heal 自动消除**的,不是永久噪声。用例已钉住。
|
||||
- **ID / Version**:旧 `Equals` 比较二者,新键在 `ID != ""` 时包含、`Version` 始终包含,等价。
|
||||
- **Condition**:两侧都走同一 `condition.Functions` 编码再递归排序,顺序无关。
|
||||
- **未知值**:`ParseBucketPolicyConfig` 用 `decoder.DisallowUnknownFields()`(`bucket-policy.go:173`),未知字段**根本无法通过解析**,因此不存在"键把未建模字段静默吃掉"的风险。
|
||||
- `isIAMPolicyReplicated`(`:3665-3681`)仍用 `Equals`,那是 IAM 策略、不同子系统,**不应**一并改动。正确地没动。
|
||||
|
||||
新旧行为是"收紧而非放宽",与收敛契约一致,判断合理。
|
||||
|
||||
### F3 —— 关闭了会吃日志的部分,但静默面仍偏大
|
||||
|
||||
已关闭:四种原因各自持 key(`site-replication-metadata.go:145/158/164/189`),真实 heal RPC 失败不再被"缺 peer"顶掉;`logger.WarningKind`(`:42`);对端"尚未拥有该桶"(`CreatedAt=0` 且载荷空、时间零)不再报。`siteReplicationStatus` 对不可达 peer 填 `DeploymentID=""`(`:3029`)落进 `BucketStats[bucket][""]`,而循环 1 按 `info.Sites` 的真实 ID 判缺失、循环 2 跳过 `id==""`,两条路径正交,语义正确。README:112-119 与实现完全一致。
|
||||
|
||||
未关闭的两点见 NB-2、NB-3。另外,"仅报告确实存在、但仍无法排序的状态"(`:157`)这个条件写得准确:`!current.valid && (len(data)!=0 || !at.IsZero())` 恰好把 F9/F10 的世代冲突暴露出来,这是意外的正收益。
|
||||
|
||||
### F5 —— 上一轮的最小性判断需要修正
|
||||
|
||||
`canonicalBucketPolicy` 的六个生产接入点应分成三类,而不是笼统的"超出必要":
|
||||
|
||||
| 接入点 | 性质 |
|
||||
|---|---|
|
||||
| `bucket-metadata-replication.go:191`(比较键) | **必需**,#77 的核心 |
|
||||
| `site-replication.go:3802`(状态键) | **必需**,F2 的修复本体 |
|
||||
| `bucket-policy-handlers.go:202`(GET)、`admin-bucket-handlers.go:438`(export)、`site-replication.go:1723`(peer apply) | **非可选**:这三处拿到的都是 `*policy.BucketPolicy`(`globalPolicySys.Get` / `GetBucketPolicy` / `admin-handlers-site-replication.go:228` 解析后传入),必须有编码器;而旧的 `json.Marshal` 对 `NotAction`/`NotResource` 语句**必然失败**——`BPStatement.Actions` 无 `omitempty`(`bucket-policy-statement.go:31`)+ `ActionSet.MarshalJSON` 空集报错(`actionset.go:144-148`)。bulk/import/peer 都能把这类策略落盘,所以缩回任何一处都会重新制造"写得进、读不出/复制不出" |
|
||||
| `bucket-policy-handlers.go:102`(PUT)、`admin-bucket-handlers.go:922`(import) | **可选的规范化**:比较键不依赖落盘字节。但因为解析器 `DisallowUnknownFields`,规范化**无信息丢失**(唯一差别是重复语句在盘上也被去重,语义等价),而所有读路径都已重新编码,用户观察不到"原样字节"。删掉它只换来与客户端提交字节的一致性,且会让本地写与 peer 写产生两种盘上形态 |
|
||||
|
||||
**结论:不建议缩回任何一处。** 真正可删的只有 PUT/import 两处,收益为零、改动风险非零。上一轮把这部分列为"范围外扩张"的说法应当被本轮判断取代;它是一个被同一改动暴露出来的真实读回缺陷的修复,且已在 README:129-134 声明。
|
||||
|
||||
---
|
||||
|
||||
## 3. 新发现(全部非阻断)
|
||||
|
||||
### NB-1(S3)恢复出的物理 Created 不确定,且可能晚于对端事件的来源时间
|
||||
|
||||
- **位置**:`cmd/bucket-metadata-replication.go:277-290`;取值链 `cmd/peer-s3-client.go:324-328`(取第一个不报错的 peer)→ `cmd/peer-s3-server.go:207-258`(`cloneDrives` 遍历 **Go map**,取第一个不报错的盘)→ `cmd/xl-storage.go:1022-1028`(`Created = st.ModTime()`)。
|
||||
- **最短触发**:桶无 `.metadata.bin`;其顶层目录 mtime 因近期对象写入被推进到接近 now;对端发来源时间早于该 mtime 的事件 → `bucket-metadata-sys.go:198-201` 判 `before-created` → **返回 nil、不写、不报错**,仅一条每小时去重的 warning,直到有人在源站重新提交。
|
||||
- **影响**:(a) 恢复值不是桶的稳定属性(目录 mtime 随顶层条目增删前进,且逐盘不同);(b) 两个站点各自独立恢复时会落进不同世代。二者都落在已声明的 F9 限制内,且相对基线(基线给每个站点各自 `UTCNow()`)是改善;本地侧因 `metadata.lock` 串行化 + 首写落盘,站内不会分裂。真正新的只有"首个跨站事件可能被静默跳过"。
|
||||
- **最小修复(可选)**:让 `ensureBucketMetadataCreated` 返回"本次是否为恢复值",当 `sourceTime` 非零且早于恢复值时取 `min(recovered, sourceTime)`——"在时刻 T 已持有配置的桶必然在 T 已存在",约 4 行,不新增字段。或退一步:只在 README 里把恢复值明确为"物理近似值,可能晚于真实创建"。
|
||||
- **需补证据**:一个用例——缓存 `Created=0`、`os.Chtimes` 把桶目录设为 now、通过 admin 路由投递 `UpdatedAt = now-1h` 的 peer 事件,断言期望行为(写入或明确跳过),把当前语义钉死。
|
||||
|
||||
### NB-2(S3)`!found` 时对畸形/不可排序状态完全静默
|
||||
|
||||
- **位置**:`cmd/site-replication-metadata.go:134-139`。
|
||||
- **最短触发**:全部站点的同一字段都无法成为候选(例如各站点都存着一份 base64 可解但内容非法的 XML,或全部世代未知)。`latestBucketConfig`(`:117-120`)对 `err != nil` 静默 `continue`,`healBucketConfig` 直接 `return nil`——**没有任何诊断**。而状态导出侧的 `logInvalid`(`site-replication.go:3375-3378`)只覆盖 base64 失败和 policy/quota/replication 的解析失败,XML 类内容非法不在其中,于是这种状态在两条路径上都无声。
|
||||
- **影响**:字段永久不可 heal 且无信号。范围窄(要求所有站点同时不可用),但属于"必要诊断"缺口。
|
||||
- **最小修复**:把每目标的 `bucketConfigStateFromInfo` 结果算一遍并缓存(当前每周期每目标实际解码两次:`latestBucketConfig` 一次、主循环一次),把 `currentErr != nil` 的 `indeterminate` 移到 `if !found` 之前。既补诊断又减一次解码,不引入新框架。
|
||||
- **需补证据**:`healBucketConfig` 单测——两个站点都给非法载荷,断言返回 nil 且产生一条 `indeterminate`(可用现有 `srStatusInfo` 夹具,参照 `site-replication-metadata-heal_test.go:136-143`)。
|
||||
|
||||
### NB-3(S4)`unreachable` 诊断按"桶 × 字段"扇出
|
||||
|
||||
- **位置**:`cmd/site-replication-metadata.go:143-147`,被 `cmd/site-replication.go:4750-4760` 每桶调用六次。
|
||||
- **最短触发**:一个站点掉线。每桶产生 6 个不同 key → 10k 桶集群每小时约 6 万条 warning。
|
||||
- **影响**:只是噪声(`logOnceIf` 按小时清理,仍算"有界"),但一个站点级事实被放大成桶×字段级输出。
|
||||
- **最小修复**:把循环 1 上提到 `healBuckets` 里、每桶调用一次(key 去掉 `file` 段),或干脆每周期一次。改动 <15 行。
|
||||
- **需补证据**:不需要新证据,行数/键数可直接核对。
|
||||
|
||||
### NB-4(S4,证据)两份旧日志是草稿测试产物,不能作为产品失败证据;设计记录"三个回归测试"应为两个
|
||||
|
||||
- **位置**:`findings-before.log:3`、`findings-after-1.log:3` 均为 `site-replication-metadata-gate_test.go:230: real physical creation time unavailable`。该消息在最终测试文件中**不存在**,最终断言在 **227** 行、消息是 `bucket without a recorded creation time cannot update %s: %v`。同理 `findings-before.log:52` 失败在 SID 断言(`:353`),而最终文件 SID 断言在 `:355`、`:353` 是 `t.Fatal(err)`。
|
||||
- **判断**:这两份日志跑的是早期草稿;其中 "real physical creation time unavailable" 更像**测试自身的前置检查**失败,不能用来证明产品缺陷。真正成立的是 `round-2/confirmed-before.log`:它用**最终测试**叠加 `before-erasure-server-pool.go` / `before-site-replication.go`(我已核对这两份确实是修正前版本:无 `NoMetadata` 早返回、`MakeBucketHook` 无回落、`syncToAllPeers` 仍用 `bucketInfo.Created`、`isBktPolicyReplicated` 仍是 `prev.Equals`),行号 227/348 与最终文件精确吻合,失败文本是真实产品错误 `bucket metadata creation time is unknown` 与 `equivalent legacy and received policy reported as permanently mismatched`,两后端 × 六类 × missing 双态全覆盖;`confirmed-after.log` 通过。
|
||||
- **另一项**:设计记录第 166 行"三个回归测试在未修复代码上失败、修复后通过"不准确。仓库中只有**两个**这样的测试;F3(heal 诊断)**没有任何自动化回归**(我在全部 `*_test.go` 中检索 `unreachable`/`peer-error`/`unusable peer`/`logBucketConfigReplication`,无命中)。F3 的唯一证据是 `runtime-after-review.log:20` 的驱动自述"40 exceptional events produced exactly one log per reason"。
|
||||
- **最小修复**:把该行改为"两个回归测试 + 一次运行时观察",并在证据索引里把 `findings-before.log` / `findings-after-1.log` 标注为 superseded 草稿。
|
||||
- **需补证据(可选)**:给 F3 补一个日志断言用例(注入 logger target 或计数 hook),否则 F3 的修复在回归套件里是无保护的。
|
||||
|
||||
### NB-5(S4)不要为死代码创造兼容理由
|
||||
|
||||
`isBucketMetadataEqual`(`cmd/site-replication.go:5172-5181`)仅被 `site-replication_test.go:156`、`bucket-cors-site-replication_test.go:484` 引用。代码里没有编造理由(很好),但设计记录第 186 行写"作为上游血缘保留"是一个不成立的说法。**最小修复**:改成"目前仅测试引用,暂不删除以缩小 diff",或连同两处测试一并删除。
|
||||
|
||||
### NB-6(S4)README 对恢复发生位置的描述偏保守
|
||||
|
||||
`docs/site-replication/README.md:106-110` 说"That recovery happens on the write path … 第一次配置写入(本地或复制而来)会记录物理时间"。实际上 `AddPeerClusters` 的初次同步(`site-replication.go:2145` → `MakeBucketHook` → 本地 `PeerBucketMakeWithVersioningHandler`)也会在本地落盘该时间。这是**低估**不是夸大,但会误导运维判断"何时脱离跳过状态"。**最小修复**:补半句"或在把该桶纳入站点复制时"。
|
||||
|
||||
### NB-7(S4)F10 只钉住了"来源"半边
|
||||
|
||||
`cmd/site-replication-metadata_test.go:473-483` 断言接管把 Created 调晚后,越过的真实字段**不能成为来源**(`!state.candidate()`)。但上一轮指出的另一半——该字段作为 **heal 目标**时,`current.valid=false` 使任何合法 incoming 无条件胜出(`site-replication-metadata.go:174` + `bucket-metadata-replication.go:239-245`)——没有钉住。我确认这半边行为存在且在"远端世代胜出"语义下是自洽的,本地用户 PUT 也总能覆盖回来(`localBucketConfigUpdatedAt` 保证 `> Created`),**不是缺陷**,但期望值应当被固定。**最小修复**:同一子用例再加一次 `applyBucketConfig(&got, file, <远端载荷>, created+shift+1m)`,断言返回 `changed=true`。
|
||||
|
||||
### NB-8(S4)恢复失败时的诊断丢掉了事件来源时间
|
||||
|
||||
`cmd/bucket-metadata-sys.go:187` 传 `time.Time{}` 作为 `at`,即使本次是携带 `sourceTime` 的 peer 事件,日志里也显示 `0001-01-01`。**最小修复**:`sourceTime != nil` 时传 `*sourceTime`(一行)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 证据分级
|
||||
|
||||
| 证据 | 评价 |
|
||||
|---|---|
|
||||
| `round-2/confirmed-before.log` + `confirmed-after.log` + `before-*.go` 覆盖文件 | **足够**。最终测试 × 旧生产代码,行号与失败文本可逐条核对,F1/F2 为已证实的产品失败 |
|
||||
| `binary-identity-after-review.json` | **足够**。`silo-final --version` 含 `commit-id=62cf066ff…`,并附基线 `5c5765816` 对照二进制与双方 sha256,上一轮 F8-4 的缺口已闭合 |
|
||||
| `final-cmd-after-review.log`(cmd 全包 622s, EXIT=0)、`ci-lint-after.log`(0 issues, 品牌基线未变)、`ci-verify-after.log`、`ci-gen/internal/s3select/crosscompile-after.log` | **足够**作为"未引入回归"的门禁证据(只截尾行,可接受) |
|
||||
| `findings-before.log`、`findings-after-1.log` | **不可用**。草稿测试产物,行号与失败文本均与最终测试不符(见 NB-4) |
|
||||
| `findings-after.log` | 仅"ok",无信息量,被 `confirmed-after.log` 覆盖 |
|
||||
| `runtime-after-review.log` | **作者自述为主**。日志由作者的双站点驱动产生,断言("six field states equal""zero metadata RPCs""exactly one log per reason")都由驱动自己判定。时间戳 06:01:50 与最终二进制构建时间 06-01-35Z 吻合,身份链成立;但它是端到端观察,不是独立验证。F3 目前**只有**这一条证据 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 设计记录(`bucket-metadata-convergence.zh.md`)的准确性
|
||||
|
||||
总体克制,未发现把限制说成已修复的地方:§rollout(gate=off 不承诺四类删除收敛)、§limits-1/2/3(历史污染、零时间事件、桶世代冲突)都如实写成限制;第 20 行明确"不能用来证明现有下载包或线上实例已具备这些能力";第 161 行主动声明交叉编译 ≠ 六平台运行验收。需要修正的只有三处,均已在上面列出:
|
||||
|
||||
1. 第 166 行"三个回归测试" → 两个回归测试 + 一次运行时观察(NB-4)。
|
||||
2. 第 186 行 `isBucketMetadataEqual`"上游血缘保留"是编造的理由(NB-5)。
|
||||
3. 第 181 行"与 `ListBuckets` 一致"成立(`ListBuckets` 在 `NoMetadata` 时同样不覆盖 Created),但第 185 行把 F5 记为"有意扩张,不是缺陷"仍沿用了上一轮的定性——按本轮判断,GET/export/peer 三处属于**必需**,建议改写,否则会给未来的人留下"这块可以缩回"的错误暗示。
|
||||
|
||||
---
|
||||
|
||||
## 6. 最小性 / 充分性 / 必要性(独立评价)
|
||||
|
||||
**最小性:是。** 本轮修正净增很小,且每一处都能对应到一个已复现的失败或一次被证实的误读:`NoMetadata` 早返回 5 行、`MakeBucketHook` 回落 3 行、`syncToAllPeers` 恢复 4 行、`isBktPolicyReplicated` 换键约 10 行、heal 诊断重排约 15 行、`isReplicatedBucketConfig` 具名化(消除"用一次性零值结构体探测类型"的晦涩写法)、README 两段。没有新 schema、新 wire 字段、新锁、新框架。我找不到可以无损删除的部分——唯一"可删"的 `canonicalBucketPolicy` PUT/import 接入是上一轮遗留,删掉反而制造两种盘上形态。
|
||||
|
||||
**充分性:对已声明契约充分,对两个边缘不充分且已如实声明。** 在"同一桶世代、Created 已知或可恢复、gate=on"范围内,六类配置的来源时间、锁范围、删除参与 heal、等时裁决、重复/乱序抑制、状态口径,我没有找到反例。不充分的两处都写进了文档:gate=off 下 Tags/SSE/Quota 漏发删除不收敛(F7);跨站点桶世代分歧不自动合并(F9,NB-1 属于它的子集)。F3 的静默面(NB-2)是本轮唯一一个"文档声明了、但我认为声明本身偏宽"的地方——"没有任何站点可供传播的字段就不产生诊断"对畸形配置而言不该成立。
|
||||
|
||||
**必要性:是。** F1 与 F2 都有"最终测试 + 修正前生产代码"的复现(`confirmed-before.log`),是确证的产品失败,不是理论推演;F3 的必要性来自 `logOnceIf` 按 key+正文去重这一可验证机制,虽无自动化回归但代码级推理成立;F4/F8/F10 是文档与证据补齐,成本近零。F5 的编码器接入经本轮重新论证后,从"可争议扩张"变为"其中三处必需、两处无损",必要性判断上调。
|
||||
|
||||
**处置建议(均非阻断,可合并后处理)**:NB-4 与 NB-5 是文字修正,建议合并前一并改掉(它们会影响后人对证据强度的判断);NB-2、NB-8 可合成一个约 20 行的提交;NB-1、NB-3、NB-6、NB-7 可进 backlog。
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"requested_model": "claude-opus-5",
|
||||
"requested_effort": "max",
|
||||
"head": "62cf066ff529c7d281703daa365f555cebba717a",
|
||||
"prior_review_head": "4089113e3edbd21a29be8e6af74662462bcc22af",
|
||||
"started_at": "2026-09-12T08:46:55.400524+00:00",
|
||||
"status": "completed",
|
||||
"completed_at": "2026-09-12T09:07:16.313535+00:00",
|
||||
"subtype": "success",
|
||||
"is_error": false,
|
||||
"duration_ms": 973153,
|
||||
"num_turns": 71,
|
||||
"assistant_models": [
|
||||
"claude-opus-5"
|
||||
],
|
||||
"used_tools": {
|
||||
"Read": 41,
|
||||
"Grep": 24,
|
||||
"Glob": 5
|
||||
},
|
||||
"stream_sha256": "63046ccfab9fb158178f43b09c75a9809a13ed1ff2da5a626e9e3c3e96c13d37",
|
||||
"review_sha256": "306a856010f7f9bd1cbec39aba83236978f5a811c98f7fc3f8a08217f72de111",
|
||||
"auxiliary_model_note": "CLI model_usage may include auxiliary calls. assistant_models identifies the actual review message model. max was explicitly passed in the recorded command.",
|
||||
"command": [
|
||||
"/opt/homebrew/bin/claude",
|
||||
"--print",
|
||||
"--model",
|
||||
"claude-opus-5",
|
||||
"--effort",
|
||||
"max",
|
||||
"--safe-mode",
|
||||
"--permission-mode",
|
||||
"plan",
|
||||
"--tools",
|
||||
"Read,Grep,Glob",
|
||||
"--strict-mcp-config",
|
||||
"--no-session-persistence",
|
||||
"--add-dir",
|
||||
"$LOCAL_EVIDENCE",
|
||||
"--add-dir",
|
||||
"$DOCS_WORKTREE/content/blog/design",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--verbose"
|
||||
],
|
||||
"command_note": "Same explicit CLI model/effort/read-only options as recorded in the tool invocation; round-2 had its own pinned worktree and prompt.",
|
||||
"session_id": "8743aaa3-5c06-43db-8eba-08302d504c4e",
|
||||
"archive_note": "Workstation paths normalized; billing data and raw model traces omitted. Original hashes are retained in archive-manifest.json."
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"head": "fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025",
|
||||
"source_clean": true,
|
||||
"built_at": "2026-09-12T09:12:38.105913+00:00",
|
||||
"binaries": {
|
||||
"silo-reviewed": {
|
||||
"sha256": "4825a801ce0ac48d636d9428ce4cd5c17a20b6cfec0b569de70a124c9c005049",
|
||||
"version": "silo-reviewed version DEVELOPMENT.2026-09-12T09-09-34Z (commit-id=fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025)\nRuntime: go1.27.1 darwin/arm64\nLicense: GNU AGPLv3 - https://www.gnu.org/licenses/agpl-3.0.html\nCopyright: 2015-2025 MinIO, Inc.\nModifications: Copyright 2025-2026 PGSTY\nSource compatibility: based on MinIO technology",
|
||||
"build_info_sha256": "91778f5f2844ada1234ed6a97d97fce7d87dc6334fdd9ef839fbeb7ebdb995e3",
|
||||
"build_info_identity_lines": [
|
||||
"\tpath\tgithub.com/minio/minio",
|
||||
"\tmod\tgithub.com/minio/minio\tv0.0.0-20260912090934-fcbb93e89572\t",
|
||||
"\tbuild\t-buildmode=exe",
|
||||
"\tbuild\t-compiler=gc",
|
||||
"\tbuild\t-trimpath=true",
|
||||
"\tbuild\tCGO_ENABLED=0",
|
||||
"\tbuild\tGOARCH=arm64",
|
||||
"\tbuild\tGOOS=darwin",
|
||||
"\tbuild\tGOARM64=v8.0",
|
||||
"\tbuild\tvcs=git",
|
||||
"\tbuild\tvcs.revision=fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025",
|
||||
"\tbuild\tvcs.time=2026-09-12T09:09:34Z",
|
||||
"\tbuild\tvcs.modified=false"
|
||||
]
|
||||
},
|
||||
"silo-before": {
|
||||
"sha256": "3b07b9604f2ca9f68f33b69f7807f57e0cc9b1efb0310bbf0aa7b3997e4483b9",
|
||||
"version": "silo-before version DEVELOPMENT.2026-09-11T18-00-57Z (commit-id=5c576581631561c446f30ae5b566f0aa793adc1c)\nRuntime: go1.27.1 darwin/arm64\nLicense: GNU AGPLv3 - https://www.gnu.org/licenses/agpl-3.0.html\nCopyright: 2015-2025 MinIO, Inc.\nModifications: Copyright 2025-2026 PGSTY\nSource compatibility: based on MinIO technology",
|
||||
"build_info_sha256": "84fb4540758d7dd56367069e3843a97d1e2c3528905db11d490b9654a2d6205d",
|
||||
"build_info_identity_lines": [
|
||||
"\tpath\tgithub.com/minio/minio",
|
||||
"\tmod\tgithub.com/minio/minio\tv0.0.0-20260911180057-5c5765816315\t",
|
||||
"\tbuild\t-buildmode=exe",
|
||||
"\tbuild\t-compiler=gc",
|
||||
"\tbuild\t-tags=kqueue",
|
||||
"\tbuild\t-trimpath=true",
|
||||
"\tbuild\tCGO_ENABLED=0",
|
||||
"\tbuild\tGOARCH=arm64",
|
||||
"\tbuild\tGOOS=darwin",
|
||||
"\tbuild\tGOARM64=v8.0",
|
||||
"\tbuild\tvcs=git",
|
||||
"\tbuild\tvcs.revision=5c576581631561c446f30ae5b566f0aa793adc1c",
|
||||
"\tbuild\tvcs.time=2026-09-11T18:00:57Z",
|
||||
"\tbuild\tvcs.modified=false"
|
||||
]
|
||||
}
|
||||
},
|
||||
"archive_note": "Dependency listing and machine paths omitted. Actual version, module identity, build settings and original build-info hash retained. Binaries are not included."
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"command": [
|
||||
"make",
|
||||
"check"
|
||||
],
|
||||
"cwd": "$DOCS_WORKTREE",
|
||||
"exit_code": 0,
|
||||
"checked_content_commit": "9fa6248a0323838df5aed678b42c4679a7a6f8a4",
|
||||
"committed_after_check": true,
|
||||
"recorded_at": "2026-09-12T09:30:37.223220+00:00",
|
||||
"files_sha256": {
|
||||
"content/blog/design/bucket-metadata-convergence.zh.md": "26807bf8db555c3f0feef5c933b3d5a2da48fc7fd5a65da00d2c9b93ce7b4b21",
|
||||
"content/blog/design/bucket-metadata-convergence.md": "36bc5c7f2310d96447b8130db0f34a99e900d8f7fc6f33d90630b89085769eec"
|
||||
},
|
||||
"rendered_pages": {
|
||||
"en": 1207,
|
||||
"zh": 1219
|
||||
},
|
||||
"checks": [
|
||||
"go mod verify",
|
||||
"strict Hugo build",
|
||||
"internal links"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
go mod verify
|
||||
all modules verified
|
||||
hugo build --minify --cleanDestinationDir --printPathWarnings --printI18nWarnings --panicOnWarning
|
||||
Start building sites …
|
||||
hugo v0.165.0+extended+withdeploy darwin/arm64 BuildDate=2026-08-12T14:26:28Z VendorInfo=Homebrew
|
||||
|
||||
|
||||
│ EN │ ZH
|
||||
──────────────────┼──────┼──────
|
||||
Pages │ 1207 │ 1219
|
||||
Paginator pages │ 2 │ 2
|
||||
Non-page files │ 26 │ 0
|
||||
Static files │ 740 │ 740
|
||||
Processed images │ 8 │ 0
|
||||
Aliases │ 45 │ 45
|
||||
Cleaned │ 0 │ 0
|
||||
|
||||
Total in 19619 ms
|
||||
python3 bin/check_internal_links.py public
|
||||
internal link check passed: 485471 rendered internal references across 1429 HTML files
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"command": [
|
||||
"go",
|
||||
"build",
|
||||
"./..."
|
||||
],
|
||||
"cwd": "$SILO_WORKTREE",
|
||||
"head": "fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025",
|
||||
"started_at": "2026-09-12T09:11:30.915932+00:00",
|
||||
"environment": {
|
||||
"TMPDIR": "$TMPDIR",
|
||||
"CGO_ENABLED": "0"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"duration_seconds": 24.193
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{"command": ["go", "build", "./..."], "cwd": "$SILO_WORKTREE", "head": "fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025", "started_at": "2026-09-12T09:11:30.915932+00:00", "environment": {"TMPDIR": "$TMPDIR", "CGO_ENABLED": "0"}}
|
||||
|
||||
EXIT=0
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"command": [
|
||||
"go",
|
||||
"test",
|
||||
"./cmd/",
|
||||
"-count=1",
|
||||
"-timeout",
|
||||
"30m"
|
||||
],
|
||||
"cwd": "$SILO_WORKTREE",
|
||||
"head": "fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025",
|
||||
"started_at": "2026-09-12T09:11:30.892660+00:00",
|
||||
"environment": {
|
||||
"TMPDIR": "$LAB_VOLUME/tmp",
|
||||
"CGO_ENABLED": "0",
|
||||
"MINIO_API_REQUESTS_MAX": "10000"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"duration_seconds": 518.124
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"command": ["go", "test", "./cmd/", "-count=1", "-timeout", "30m"], "cwd": "$SILO_WORKTREE", "head": "fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025", "started_at": "2026-09-12T09:11:30.892660+00:00", "environment": {"TMPDIR": "$LAB_VOLUME/tmp", "CGO_ENABLED": "0", "MINIO_API_REQUESTS_MAX": "10000"}}
|
||||
ok github.com/minio/minio/cmd 492.776s
|
||||
|
||||
EXIT=0
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"command": [
|
||||
"make",
|
||||
"lint"
|
||||
],
|
||||
"cwd": "$SILO_WORKTREE",
|
||||
"head": "461e9a721047c63e1a95f54ad4b533a6b89def30",
|
||||
"started_at": "2026-09-12T09:17:19.334284+00:00",
|
||||
"environment": {
|
||||
"TMPDIR": "$TMPDIR"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"duration_seconds": 79.064
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"command": ["make", "lint"], "cwd": "$SILO_WORKTREE", "head": "461e9a721047c63e1a95f54ad4b533a6b89def30", "started_at": "2026-09-12T09:17:19.334284+00:00", "environment": {"TMPDIR": "$TMPDIR"}}
|
||||
Running lint check
|
||||
0 issues.
|
||||
typos binary is not found.. skipping..
|
||||
|
||||
EXIT=0
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"command": [
|
||||
"go",
|
||||
"run",
|
||||
".issue77-evidence/twosite/main.go",
|
||||
"$LAB_VOLUME/runtime-reviewed",
|
||||
".issue77-evidence/silo-reviewed",
|
||||
".issue77-evidence/silo-before"
|
||||
],
|
||||
"cwd": "$SILO_WORKTREE",
|
||||
"head": "fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025",
|
||||
"started_at": "2026-09-12T09:12:41.365111+00:00",
|
||||
"environment": {
|
||||
"TMPDIR": "$LAB_VOLUME/tmp"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"duration_seconds": 190.476
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{"command": ["go", "run", ".issue77-evidence/twosite/main.go", "$LAB_VOLUME/runtime-reviewed", ".issue77-evidence/silo-reviewed", ".issue77-evidence/silo-before"], "cwd": "$SILO_WORKTREE", "head": "fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025", "started_at": "2026-09-12T09:12:41.365111+00:00", "environment": {"TMPDIR": "$LAB_VOLUME/tmp"}}
|
||||
2026-09-12T09:12:42Z site-a ready gate=on
|
||||
2026-09-12T09:12:43Z site-b ready gate=on
|
||||
2026-09-12T09:12:44Z historical bucket prepared: six live fields at Created
|
||||
2026-09-12T09:12:44Z baseline-initial-sync: waiting for ordinary 30-second heal
|
||||
2026-09-12T09:12:44Z baseline-initial-sync: six field states equal
|
||||
2026-09-12T09:12:44Z initial sync preserved six historical Created timestamps
|
||||
2026-09-12T09:12:44Z real local PUT outgoing replication RPC dropped by partition
|
||||
2026-09-12T09:12:45Z live-reconnect: waiting for ordinary 30-second heal
|
||||
2026-09-12T09:13:13Z live-reconnect: six field states equal
|
||||
2026-09-12T09:13:13Z live-steady: observing two heal intervals
|
||||
2026-09-12T09:14:18Z live-steady: zero metadata RPCs across 65 seconds
|
||||
2026-09-12T09:14:18Z six duplicate/out-of-order events rejected without clock changes
|
||||
2026-09-12T09:14:19Z real local DELETE outgoing replication RPC dropped by partition
|
||||
2026-09-12T09:14:20Z site-a ready gate=on
|
||||
2026-09-12T09:14:20Z four tombstones survived process restart
|
||||
2026-09-12T09:14:20Z delete-reconnect: waiting for ordinary 30-second heal
|
||||
2026-09-12T09:14:44Z delete-reconnect: six field states equal
|
||||
2026-09-12T09:14:44Z delete-steady: observing two heal intervals
|
||||
2026-09-12T09:15:49Z delete-steady: zero metadata RPCs across 65 seconds
|
||||
2026-09-12T09:15:49Z 40 exceptional events produced exactly one log per reason
|
||||
2026-09-12T09:15:49Z starting fixed/previous-server rolling-upgrade smoke with gate off
|
||||
2026-09-12T09:15:50Z site-a ready gate=off
|
||||
2026-09-12T09:15:51Z site-b ready gate=off
|
||||
2026-09-12T09:15:51Z mixed-version ordinary PUT/DELETE and off tombstone visibility passed
|
||||
2026-09-12T09:15:51Z PASS: isolated two-site implementation acceptance
|
||||
|
||||
EXIT=0
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"command": [
|
||||
"go",
|
||||
"test",
|
||||
"-race",
|
||||
"./cmd/",
|
||||
"-run",
|
||||
"^(TestBucketMetadataInitialSyncPhysicalCreated|TestHealBucketConfigDiagnostics|TestHealBucketConfigWithoutSourceDiagnostics)$",
|
||||
"-count=1",
|
||||
"-timeout",
|
||||
"10m"
|
||||
],
|
||||
"cwd": "$SILO_WORKTREE",
|
||||
"head": "461e9a721047c63e1a95f54ad4b533a6b89def30",
|
||||
"started_at": "2026-09-12T09:18:02.407600+00:00",
|
||||
"environment": {
|
||||
"TMPDIR": "$LAB_VOLUME/tmp",
|
||||
"CGO_ENABLED": "1"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"duration_seconds": 45.681
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"command": ["go", "test", "-race", "./cmd/", "-run", "^(TestBucketMetadataInitialSyncPhysicalCreated|TestHealBucketConfigDiagnostics|TestHealBucketConfigWithoutSourceDiagnostics)$", "-count=1", "-timeout", "10m"], "cwd": "$SILO_WORKTREE", "head": "461e9a721047c63e1a95f54ad4b533a6b89def30", "started_at": "2026-09-12T09:18:02.407600+00:00", "environment": {"TMPDIR": "$LAB_VOLUME/tmp", "CGO_ENABLED": "1"}}
|
||||
ok github.com/minio/minio/cmd 4.540s
|
||||
|
||||
EXIT=0
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"command": [
|
||||
"go",
|
||||
"test",
|
||||
"-race",
|
||||
"./cmd/",
|
||||
"-run",
|
||||
"^(TestBucketMetadataInitialSyncPhysicalCreated|TestPeerBucketMetadataPhysicalCreatedBoundary|TestPeerBucketMetadataUnknownCreated|TestPeerBucketAdoptionRebasesOnlyDefaults|TestHealBucketConfigDiagnostics|TestHealBucketConfigWithoutSourceDiagnostics|TestBucketMetadataPhysicalCreatedRecovery|TestBucketPolicyReplicationStatusLegacyOrder|TestHealBucketConfigSourceAndQuiescence|TestLatestBucketConfigCandidates|Test.*Cors.*|Test.*CORS.*)$",
|
||||
"-count=1",
|
||||
"-timeout",
|
||||
"15m"
|
||||
],
|
||||
"cwd": "$SILO_WORKTREE",
|
||||
"head": "fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025",
|
||||
"started_at": "2026-09-12T09:11:30.873027+00:00",
|
||||
"environment": {
|
||||
"TMPDIR": "$LAB_VOLUME/tmp",
|
||||
"CGO_ENABLED": "1"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"duration_seconds": 90.202
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"command": ["go", "test", "-race", "./cmd/", "-run", "^(TestBucketMetadataInitialSyncPhysicalCreated|TestPeerBucketMetadataPhysicalCreatedBoundary|TestPeerBucketMetadataUnknownCreated|TestPeerBucketAdoptionRebasesOnlyDefaults|TestHealBucketConfigDiagnostics|TestHealBucketConfigWithoutSourceDiagnostics|TestBucketMetadataPhysicalCreatedRecovery|TestBucketPolicyReplicationStatusLegacyOrder|TestHealBucketConfigSourceAndQuiescence|TestLatestBucketConfigCandidates|Test.*Cors.*|Test.*CORS.*)$", "-count=1", "-timeout", "15m"], "cwd": "$SILO_WORKTREE", "head": "fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025", "started_at": "2026-09-12T09:11:30.873027+00:00", "environment": {"TMPDIR": "$LAB_VOLUME/tmp", "CGO_ENABLED": "1"}}
|
||||
ok github.com/minio/minio/cmd 20.903s
|
||||
|
||||
EXIT=0
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"reviewed_production_head": "fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025",
|
||||
"final_head": "461e9a721047c63e1a95f54ad4b533a6b89def30",
|
||||
"changed_files": [
|
||||
"cmd/site-replication-metadata-gate_test.go",
|
||||
"cmd/site-replication-metadata-heal_test.go"
|
||||
],
|
||||
"difference": "Test-only gofumpt formatting and equivalent if-chain to switch; no production or server documentation changes",
|
||||
"patch": "test-style.diff"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"command": [
|
||||
"go",
|
||||
"vet",
|
||||
"./..."
|
||||
],
|
||||
"cwd": "$SILO_WORKTREE",
|
||||
"head": "fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025",
|
||||
"started_at": "2026-09-12T09:11:55.197305+00:00",
|
||||
"environment": {
|
||||
"TMPDIR": "$TMPDIR",
|
||||
"CGO_ENABLED": "0"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"duration_seconds": 4.024
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{"command": ["go", "vet", "./..."], "cwd": "$SILO_WORKTREE", "head": "fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025", "started_at": "2026-09-12T09:11:55.197305+00:00", "environment": {"TMPDIR": "$TMPDIR", "CGO_ENABLED": "0"}}
|
||||
|
||||
EXIT=0
|
||||
@@ -0,0 +1,111 @@
|
||||
已完成定向核对。以下为最终报告。
|
||||
|
||||
---
|
||||
|
||||
# SILO #77 第三轮独立对抗性验收(固定 `fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025`)
|
||||
|
||||
**结论:GO_WITH_NONBLOCKING_NOTES — 阻断数 0**
|
||||
|
||||
范围限定本轮差异(`round-3/change.diff`:2 个生产文件共约 12 行、5 个测试文件、1 个 README 段落)。未重复前两轮已完成的全量审查。
|
||||
|
||||
---
|
||||
|
||||
## 一、逐条 NB 判定
|
||||
|
||||
### NB-2 — 关闭(三个边界经代码级核对成立)
|
||||
|
||||
生产改动在 `cmd/site-replication-metadata.go:134-160`。逐边界挑战结果:
|
||||
|
||||
1. **`unreachable` 仅在存在候选时输出 —— 行为字节级不变**。改前 `if !found { return nil }` 就在 unreachable 循环之前,因此该日志从来只在 `found==true` 时产生;把它包进 `if found {}`(`:138-144`)是纯重构,没有新增也没有删除任何一条 unreachable。这一点上一轮报告与提交说明都没讲清,值得记录:**这不是行为修改**。
|
||||
2. **空基线/缺桶安静 —— 由 `newBucketConfigState` 的取值规则保证**(`cmd/bucket-metadata-replication.go:224-227`):`at.IsZero() → at = created`。因此"字段时间为零但 `CreatedAt` 非零"的合法基线必然 `valid=true`,不触发 `:154`;缺桶(全零)`data` 空且 `at` 零,也不触发。只有三类会响:`created==0 且 at/data 非空`、`at < created`、解码/解析报错。与新测试四个子用例一一对应,无第四种漏网情形。
|
||||
3. **无有效来源不发 RPC、不落盘 —— 成立**。`!found` 在 `:159` 即 `continue`,位于 `:177`(本地 `updateAndParseMetadata`)与 `:182`(`SRPeerReplicateBucketMeta`)之前。测试用 `len(events()) != 0` 从对端侧反证(`heal_test.go:162-164`)。
|
||||
4. **去重 —— 成立**。`logOnceIf`(`internal/logger/logonce.go:95-120`)按 `id` **且**错误正文相同才抑制;key 为 `bucket-metadata/<bucket>/<file>/<reason>`,正文只含 reason,时间/peer 全在 ReqInfo 属性里(`site-replication-metadata.go:33-43`)。所以多 peer 同因塌缩为一条,跨因不互相顶替。测试的 `for range 2` 钉住了这一点。
|
||||
|
||||
**必要性证据成立**:`round-2/no-source-before.log` 用同一份最终测试叠加旧生产码,`unknown-created`/`malformed`/`before-created` 三个子用例在 `heal_test.go:155` 报 `got 0 diagnostics, want 1`,`empty-baseline` 两侧都安静。行号与当前文件精确吻合,是真实的前后对照,不是草稿产物。
|
||||
|
||||
**接受的边界(非阻断,已成文)**:在全站 `Created==0` 的遗留桶上(滚动升级的典型形态),每个"有值字段"现在每小时多出一条 `indeterminate`,此前完全静默。这正是 NB-2 要的可行动信号,README:123-125 与设计记录 §limits-5 都写明了。量级与 NB-3 已接受的 `unreachable` 同阶。
|
||||
|
||||
**性能挑战结果:不构成问题**。`!found` 时主循环多做一遍 `bucketConfigStateFromInfo`,但空载荷在 `site-replication-metadata.go:75` 与 `bucket-metadata-replication.go:178-180` 两处短路,连 base64 都不解——而空载荷恰好是 `!found` 的主体人群。上一轮建议的"缓存解码结果"因此确实可以不做。
|
||||
|
||||
### NB-8 — 关闭,nil/zero/local 三分支均无错误时间与 panic
|
||||
|
||||
`cmd/bucket-metadata-sys.go:186-192`:
|
||||
|
||||
- `sourceTime == nil` → `at` 为零值,打印 `0001-01-01`,与改前一致,无解引用;
|
||||
- `sourceTime != nil && IsZero()` → 同上,不会把零值当作有效来源时间;
|
||||
- `sourceTime` 非零且为本地时区 → `logBucketConfigReplication:38` 统一 `at.UTC().Format(...)`,不产生偏移时间;
|
||||
- `meta.Created`:该分支下必为零(`ensureBucketMetadataCreated:278-280` 在非零时提前返回 nil),所以 `created` 属性如实;
|
||||
- 去重不受影响:错误正文未变(见上)。
|
||||
|
||||
**一处同类残留(本轮之前既有,非本轮引入)**:`site-replication-metadata.go:155` 在 `currentErr != nil` 时 `current` 是零值结构体,于是"解析失败"的日志同样丢掉了对端上报的真实 `at`。最小修复需要一个不经解码就取 `(bucket,file) → at` 的取值函数,约 10 行。可选,不必本轮做。
|
||||
|
||||
### NB-4 / NB-5 — 关闭;全仓无漏引用,编译成立;有一项必须如实指出的门禁事实
|
||||
|
||||
- **漏引用**:`isBucketMetadataEqual` 在 Go 代码中零引用,仅 `docs/site-replication/CORS-LWW-DESIGN.md:84` 提及,且位于 "Confirmed Failures in the **Pre-Fix** Candidate" 小节,是对既往缺陷的历史陈述,不是对现有代码的断言,无需改。
|
||||
- **编译**:两个被删用例所在文件的 `encoding/base64` 导入仍在被使用(`site-replication_test.go:21` → `:80/:106`;`bucket-cors-site-replication_test.go:22` → 20 余处),不存在第二次漏删。`round-3/final-build.log`、`final-vet.log` 在本轮固定 SHA 上 `go build ./...` / `go vet ./...` 均 `EXIT=0`。**当前树完整编译成立**。
|
||||
- **真实 CORS 路径保留**:被删的是纯 helper 单测;同一性质(base64 必须按字节严格比较)现由 `TestPeerBucketCorsRejectsNonCanonicalBase64`(`bucket-cors-site-replication_test.go:782-815`)经真实 `PeerBucketCorsConfigHandler` 与 legacy bulk 两条生产路径覆盖,并断言被拒后元数据未变;heal 侧用 `corsReplicationStateFromInfo`/`equalCORSReplicationStates`(`site-replication.go:4903-4904`)而非被删函数。**无覆盖损失。**
|
||||
- **logger 全局状态与 race**:`testlogger.T` 是进程级单例但用 atomic(`testlogger.go:49,78-85`),本身无 race;`logger.DisableLog` 是普通全局 bool(`logger.go:407` 读、`test-utils_test.go:105` 默认置 true),两个新用例都 `defer` 还原。`round-3/final-target-race.log` 在本轮固定 SHA 上对包含全部新用例的集合 `-race` 跑通(20.9s,EXIT=0)。**已验证,非阻断。**
|
||||
- **两项潜在脆弱点(无阻断、目前未触发,仅备案)**:(a) `DisableLog=false` 期间全局 sink 会捕获进程内**任何**系统日志,而 `heal_test.go:97` 对任何非预期行直接 `Fatalf`——若窗口内有后台 goroutine 输出日志即失败。最小加固:分类前先 `strings.Contains(line, bucket)` 过滤外来日志。(b) `logOnce.IDMap` 是进程级、每小时才清(`logonce.go:123-131`),这两个用例之所以安全,是因为 `ExecObjectLayerAPITest` 对两种后端各自 `getRandomBucketName()`(`test-utils_test.go:1556`,分别在 `:1771`/`:1801` 调用)使日志 key 天然唯一——这是隐式依赖,值得在注释里点一句。
|
||||
- **证据精度**:`round-2/confirmed-diagnostics-before.log` 在 `heal_test.go:80`(**第一条**断言)就 Fatal,因此它只证明了 F3 的"空基线必须安静"这一半;WARNING 级别与按因去重(`:99-105`)在旧码上并未被执行到,**没有**对应的失败演示。应如实这么写,不能说成"F3 已被完整回归保护"。
|
||||
|
||||
- **必须指出的门禁事实**:`round-3/pre-style-lint.log` 显示本轮固定提交 `fcbb93e8` 的 `make lint` **失败(EXIT=2,3 issues)**,全部落在本轮新增的两个测试文件:`cmd/site-replication-metadata-heal_test.go:92:5`(gocritic ifElseChain)、`cmd/site-replication-metadata-gate_test.go:282:1` 与 `heal_test.go:138:1`(gofumpt)。我通读了修复补丁 `test-style.diff`:switch 分支顺序与函数体同 if-else 完全一致,另两处仅为结构体字面量换行,**语义等价、仅测试文件**,已在后续提交 `461e9a72` 修掉(该提交的 lint 复跑在证据中仍在进行)。结论:不影响行为验收,但"`fcbb93e8` 本身通过全部仓库门禁"这句话不成立,不应这么写。
|
||||
|
||||
### NB-6 — 关闭为**出站 wire 证明**;不得称作双站点证明(已如实区分)
|
||||
|
||||
`TestBucketMetadataInitialSyncPhysicalCreated`(`gate_test.go:245-303`)确实驱动真实源 ObjectLayer 走完整出站序列:`syncToAllPeers`(`site-replication.go:2141-2153`)→ `ensureBucketMetadataCreated` 从真实盘目录 mtime 恢复 → `MakeBucketHook`(`:821-826` 缓存零值回落)→ wire 上的 `createdAt` 参数;再经 `initialBucketConfigReplicationEvent` 发出 Tags 事件。断言 `createdAt == physical`、Tags 载荷为 base64(XML) 且 `UpdatedAt == physical`(历史字段以 Created 作源时间)。前后对照成立:`round-2/initial-sync-before.log` 在 `gate_test.go:289` 报 `"0001-01-01T00:00:00Z"`,行号与最终文件精确吻合。
|
||||
|
||||
**必须如实标注的范围限制(我独立核出,不在提交说明里)**:该用例的 `c.state.Peers` 只含 `"initial-peer"`(`gate_test.go:282-283`),而 `concDo`(`site-replication.go:2497-2515`)只对 `depID == globalDeploymentID()` 的条目执行 `selfActionFn`。该条目不存在,因此真正把 `Created` 落到**本地盘**的 `PeerBucketMakeWithVersioningHandler`(`site-replication.go:954-966` 的 `SetCreatedAt` + `saveMetadata`)**在本用例中根本没有执行**。也就是说:
|
||||
|
||||
- README:110-114 "Recovery happens … during initial site sync … records the physical time" 在生产路径上成立(经 `concDo` 自身分支),但**不被这个测试覆盖**;
|
||||
- 本用例证明的是出站内容,不是任何一侧的落盘,更不是双站点收敛。
|
||||
|
||||
设计记录 §205 已经把这个区分写对了("它证明出站内容…二者不能混称为同一验收"),README 也没有声称测试覆盖,因此**不构成不实陈述**。若要补齐本地落盘那一半:在该 Peers map 中加入 `globalDeploymentID(): {}` 并在末尾加一次 `readBucketMetadata` 断言 `Created==physical`,约 3 行。
|
||||
|
||||
### NB-7 — 关闭,两半都钉住了
|
||||
|
||||
`site-replication-metadata_test.go:482-490`。沿 `applyBucketConfig`(`bucket-metadata-replication.go:294-316`)+ `compareBucketConfigStates`(`:239-245`)核对:`incoming.valid=true` 对 `local.valid=false` 直接返回 1,因此 `changed=true`,随后断言指针别名上的 `*at == incomingAt`、`*data == incoming`——载荷与时间都断言了。对 `bucketTaggingConfig` 这是"活标签覆盖被保留的旧世代删除记录",语义与注释一致;对 `bucketSSEConfig` 载荷恰好与原值相同,那里真正承载证明的是时间戳断言。两点说明:断言层级是 `applyBucketConfig`(即写路径 `bucket-metadata-sys.go:206` 调用的同一决策函数),属单元级钉桩而非端到端,这是可接受的取舍。
|
||||
|
||||
### NB-1 — 判定为**如实保留边界**,本轮不存在必须修的回归
|
||||
|
||||
本轮对 NB-1 **没有任何生产代码改动**(仅 README:106-114 + 新用例),因此按构造不可能引入回归。我独立复核了被保留的行为与被拒方案:
|
||||
|
||||
- 保留跳过:`bucket-metadata-sys.go:202-205` 在 `updatedAt.Before(meta.Created)` 时记 `before-created` 并 `return nil`(不写、不报错);heal 侧同因在 `site-replication-metadata.go:162-164`。
|
||||
- 拒绝 `min(recovered, sourceTime)` 的理由我认为**成立**:单个事件的时间戳无法区分"本世代的较早事件"与"上一个已删除世代的事件",凭一个事件下调 `Created` 会让删除前的状态复活。这里选安全而非活性是对的,且具备自愈性(下一次 ≥ physical 的写入即确立身份)与可观测性(有界 warning)。
|
||||
- 新用例 `TestPeerBucketMetadataPhysicalCreatedBoundary`(`gate_test.go:305-335`)用真实盘 mtime 同时钉住两侧:`physical-1h` 的 peer 事件 `err==nil` 且 `bucketConfigWriteCounter` 计数为 **0**(是跳过不是失败);随后本地 `Update` 使盘上 `Created==physical` 且 `at > physical`。断言选得准确。
|
||||
- 残留(已成文):唯一持有者的源时间早于恢复值的字段,不经人工介入不会收敛;且跳过路径**不落盘恢复值**(`:204` 早于 `saveMetadata`),每次事件都重新探测 mtime——README:112-113 用 "first **successful** configuration write" 措辞正确地表达了这一点。
|
||||
|
||||
### NB-3 — 接受现状,不要求扩展
|
||||
|
||||
`site-replication-metadata.go:138-144` 仍按桶×字段×原因输出,`healBuckets` 每桶调用六次(`site-replication.go:4753-4760` → `:4851-4935`)。设计记录 §limits-5 已明确声明该量级会随桶与有值字段增长。未构成阻断,也未破坏任何既定契约,按约定不要求做站点级聚合/限流改造。
|
||||
|
||||
**一处文档精度小瑕**:§limits-5 把"日志量随桶与字段增长"这句只挂在"不可达站点"上;本轮之后,遗留零 `Created` 集群里的 `indeterminate` 同样按桶×字段增长。加半句即可,非阻断。
|
||||
|
||||
### F5 — 确认无外溢
|
||||
|
||||
本轮 diff 中没有任何 `canonicalBucketPolicy` 接入点的增删;只有设计记录 F5 一行表述更新(`bucket-metadata-convergence.md:182` / `.zh.md:185`),与"上轮已修正原判断、不再改代码"一致。
|
||||
|
||||
---
|
||||
|
||||
## 二、确证问题清单(均非阻断)
|
||||
|
||||
| # | 位置 | 触发条件 | 最小修复 |
|
||||
|---|---|---|---|
|
||||
| 1 | `cmd/site-replication-metadata-heal_test.go:92`、`:138`;`cmd/site-replication-metadata-gate_test.go:282` | 在固定提交 `fcbb93e8` 上执行 `make lint` | 已在 `461e9a72` 修掉(语义等价、仅测试文件);本轮 SHA 需注明 lint 未过 |
|
||||
| 2 | `cmd/site-replication-metadata.go:155` | heal 遇到解码/解析失败的对端状态 | 日志属性中 `sourceTime` 恒为 `0001-01-01`;需一个免解码的 `(bucket,file)→at` 取值函数,约 10 行。可选 |
|
||||
| 3 | `cmd/site-replication-metadata-heal_test.go:97` | 日志窗口内任一后台 goroutine 输出日志 | 分类前加 `strings.Contains(line, bucket)` 过滤,1 行 |
|
||||
| 4 | `cmd/site-replication-metadata-gate_test.go:282-283` | —(覆盖缺口,非缺陷) | 若要覆盖 README 的"初次同步落盘 Created"一半:Peers 加入 `globalDeploymentID()` 并补一次 `readBucketMetadata` 断言,约 3 行 |
|
||||
| 5 | 设计记录 §limits-5 | — | "随桶与字段增长"补上 `indeterminate` 这一类,半句 |
|
||||
|
||||
**证据状态如实说明**:`round-3/final-cmd.log` 只有头行,**完整 `go test ./cmd/` 尚未产生结果**,不可称作已通过;已完成且可复核的是 `final-build`/`final-vet`(EXIT=0)、`final-target-race`(EXIT=0)、`pre-style-lint`(EXIT=2,见上)。`461e9a72` 的 `make lint` 复跑同样在进行中。
|
||||
|
||||
---
|
||||
|
||||
## 三、最小性 / 充分性 / 必要性(独立评价)
|
||||
|
||||
**最小性:是,且是本系列迄今最克制的一轮。** 生产侧净变化只有两处共约 12 行:`found` 条件位移(未引入解码缓存、未新增结构体、未新增日志原因),以及诊断里一个已有指针的解引用。没有新 schema、新 wire 字段、新锁、新框架。我找不到可以无损删除的部分:删掉 `!found` 分支就退回 NB-2 的静默,删掉 `at` 就退回 NB-8。测试删除项(`isBucketMetadataEqual` 及其两个专属用例)经全仓核对确认无生产调用者、无覆盖损失。
|
||||
|
||||
**充分性:对本轮声称的目标充分;两处覆盖如实低于文字。** 新增三个用例都有真实前后对照或真实后端驱动,`-race` 通过。低于文字的两处均已在上文点名:F3 的级别/去重断言在旧码上未被演示失败;初次同步的本地落盘半边未被测试覆盖(生产路径本身成立)。二者都是证据强度问题,不是行为问题,且设计记录没有把它们说过头。
|
||||
|
||||
**必要性:是。** NB-2 与 NB-6 各有"最终测试 + 旧生产码"的复现日志且行号可逐条核对(`no-source-before.log`、`initial-sync-before.log`);NB-8 是一行可读性缺陷的直接消除;NB-5 是删除无调用者代码;NB-7 是补一侧期望值;NB-1/NB-3 明确不改代码,只做文档与钉桩。没有一项属于"顺手扩张"。
|
||||
|
||||
**放行建议**:以 `461e9a72`(lint 修复后的等价树)为合并基准,并在合入前等 `go test ./cmd/` 与 `make lint` 两条复跑各自出结果;上表 5 项可全部进 backlog。
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"requested_model": "claude-opus-5",
|
||||
"requested_effort": "max",
|
||||
"head": "fcbb93e8957275bfa7ad4e6154e93f4d7b0a7025",
|
||||
"base": "62cf066ff529c7d281703daa365f555cebba717a",
|
||||
"status": "completed",
|
||||
"started_at": "2026-09-12T09:10:48.496423+00:00",
|
||||
"command": [
|
||||
"/opt/homebrew/bin/claude",
|
||||
"--print",
|
||||
"--model",
|
||||
"claude-opus-5",
|
||||
"--effort",
|
||||
"max",
|
||||
"--safe-mode",
|
||||
"--permission-mode",
|
||||
"plan",
|
||||
"--tools",
|
||||
"Read,Grep,Glob",
|
||||
"--strict-mcp-config",
|
||||
"--no-session-persistence",
|
||||
"--add-dir",
|
||||
"$LOCAL_EVIDENCE",
|
||||
"--add-dir",
|
||||
"$DOCS_WORKTREE/content/blog/design",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--verbose"
|
||||
],
|
||||
"completed_at": "2026-09-12T09:25:59.617894+00:00",
|
||||
"subtype": "success",
|
||||
"is_error": false,
|
||||
"duration_ms": 777403,
|
||||
"num_turns": 61,
|
||||
"assistant_models": [
|
||||
"claude-opus-5"
|
||||
],
|
||||
"used_tools": {
|
||||
"Read": 33,
|
||||
"Grep": 23,
|
||||
"Glob": 4
|
||||
},
|
||||
"stream_sha256": "a9a8784005abe5f02c1847e6d2a42890a309dbf34604a3756c456f1fc29c612a",
|
||||
"review_sha256": "9106938a7a34cfbc6a11d372dd5ffb71ae7417a77a56d10bbed178f539de8fba",
|
||||
"auxiliary_model_note": "CLI model_usage may include auxiliary calls. assistant_models identifies the actual review message model. max was explicitly passed in the recorded command.",
|
||||
"archive_note": "Workstation paths normalized; billing data and raw model traces omitted. Original hashes are retained in archive-manifest.json.",
|
||||
"session_id": "3b2ce46c-226b-4c52-b004-c727303c4a9b",
|
||||
"session_id_source": "system init event in the stream identified by stream_sha256"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
diff --git a/cmd/site-replication-metadata-gate_test.go b/cmd/site-replication-metadata-gate_test.go
|
||||
index 49818b548..de9a37bb6 100644
|
||||
--- a/cmd/site-replication-metadata-gate_test.go
|
||||
+++ b/cmd/site-replication-metadata-gate_test.go
|
||||
@@ -279,8 +279,10 @@ func TestBucketMetadataInitialSyncPhysicalCreated(t *testing.T) {
|
||||
defer peer.Close()
|
||||
// Exercise the complete outgoing sync sequence with real source
|
||||
// storage. This peer acknowledges RPCs; it is not a second ObjectLayer.
|
||||
- c := &SiteReplicationSys{enabled: true, state: srState{ServiceAccountAccessKey: serviceCred.AccessKey,
|
||||
- Peers: map[string]madmin.PeerInfo{"initial-peer": {DeploymentID: "initial-peer", Endpoint: peer.URL}}}}
|
||||
+ c := &SiteReplicationSys{enabled: true, state: srState{
|
||||
+ ServiceAccountAccessKey: serviceCred.AccessKey,
|
||||
+ Peers: map[string]madmin.PeerInfo{"initial-peer": {DeploymentID: "initial-peer", Endpoint: peer.URL}},
|
||||
+ }}
|
||||
if err := c.syncToAllPeers(ctx, madmin.SRAddOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
diff --git a/cmd/site-replication-metadata-heal_test.go b/cmd/site-replication-metadata-heal_test.go
|
||||
index bedb3ea5e..6c00fb8c1 100644
|
||||
--- a/cmd/site-replication-metadata-heal_test.go
|
||||
+++ b/cmd/site-replication-metadata-heal_test.go
|
||||
@@ -89,11 +89,12 @@ func TestHealBucketConfigDiagnostics(t *testing.T) {
|
||||
defer capture.mu.Unlock()
|
||||
var unreachable, peerError int
|
||||
for _, line := range capture.lines {
|
||||
- if strings.Contains(line, "bucket metadata replication: unreachable") {
|
||||
+ switch {
|
||||
+ case strings.Contains(line, "bucket metadata replication: unreachable"):
|
||||
unreachable++
|
||||
- } else if strings.Contains(line, "bucket metadata replication: peer-error") {
|
||||
+ case strings.Contains(line, "bucket metadata replication: peer-error"):
|
||||
peerError++
|
||||
- } else {
|
||||
+ default:
|
||||
t.Fatalf("unexpected diagnostic: %s", line)
|
||||
}
|
||||
if !strings.HasPrefix(line, "WARNING:") {
|
||||
@@ -135,11 +136,13 @@ func TestHealBucketConfigWithoutSourceDiagnostics(t *testing.T) {
|
||||
// and neither local storage nor the recording peer may be written.
|
||||
name := bucket + "-" + tc.name
|
||||
local := globalDeploymentID()
|
||||
- info := srStatusInfo{Sites: map[string]madmin.PeerInfo{local: {}, "metadata-peer": {}, "unreachable": {}},
|
||||
+ info := srStatusInfo{
|
||||
+ Sites: map[string]madmin.PeerInfo{local: {}, "metadata-peer": {}, "unreachable": {}},
|
||||
BucketStats: map[string]map[string]srBucketStatsSummary{name: {
|
||||
local: bucketConfigTestInfo(name, bucketTaggingConfig, tc.data, tc.at, tc.created),
|
||||
"metadata-peer": {},
|
||||
- }}}
|
||||
+ }},
|
||||
+ }
|
||||
for range 2 {
|
||||
if err := globalSiteReplicationSys.healBucketConfig(t.Context(), name, bucketTaggingConfig, info); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"requested_model": "claude-opus-5",
|
||||
"requested_effort": "max",
|
||||
"cli_version": "2.1.258",
|
||||
"base": "5c576581631561c446f30ae5b566f0aa793adc1c",
|
||||
"head": "4089113e3edbd21a29be8e6af74662462bcc22af",
|
||||
"started_at": "2026-09-12T04:02:26.329601+00:00",
|
||||
"status": "completed",
|
||||
"observed_model": "claude-opus-5",
|
||||
"session_id": "86133939-ee59-4e4f-ad98-61f825c91899",
|
||||
"permission_mode": "plan",
|
||||
"tools": [
|
||||
"Glob",
|
||||
"Grep",
|
||||
"Read"
|
||||
],
|
||||
"command": "claude --print --model claude-opus-5 --effort max --safe-mode --permission-mode plan --tools Read,Grep,Glob --strict-mcp-config --no-session-persistence --add-dir $LOCAL_EVIDENCE --output-format stream-json --verbose",
|
||||
"prompt_sha256": "09cea2c1356a2261e8cb9b0d4f2c947d0b683d00386267ebb340d487bdc66b4d",
|
||||
"diff_sha256": "58703d70978fa25ea6b23eb6735ec94a6d369df3a776e5169a13c847f0d6b0eb",
|
||||
"completed_at": "2026-09-12T04:35:09.936197+00:00",
|
||||
"subtype": "success",
|
||||
"is_error": false,
|
||||
"duration_ms": 1841224,
|
||||
"num_turns": 93,
|
||||
"assistant_models": [
|
||||
"claude-opus-5"
|
||||
],
|
||||
"used_tools": {
|
||||
"Read": 55,
|
||||
"Grep": 31,
|
||||
"Glob": 5,
|
||||
"Write": 1
|
||||
},
|
||||
"stream_sha256": "90a262f5cc189575b2e8b458c8e5cd17274fb493261c35135d199bee4895de24",
|
||||
"review_sha256": "e40dad6e1969898e35c82b7fd24517a5ee714d603a54fe3598b9e69c20922118",
|
||||
"auxiliary_model_note": "CLI model_usage may include auxiliary calls. assistant_models identifies the actual review message model. max was explicitly passed in the recorded command.",
|
||||
"archive_note": "Workstation paths normalized; billing data and raw model traces omitted. Original hashes are retained in archive-manifest.json.",
|
||||
"source_tree_equivalent_commit": "1ee64a8d8",
|
||||
"tool_scope_note": "Configured Read/Grep/Glob. The CLI also attempted a Write to its own plan file; no production source was edited by the reviewer."
|
||||
}
|
||||
Reference in New Issue
Block a user